Compare commits

..

25 Commits

Author SHA1 Message Date
Tina
cd92e4cf6f fix: Revert "feat: trace jsonrpc (#6159)" (#6190)
Revert "feat: trace jsonrpc (#6159)"

This reverts commit 6fee37c4b8.
2023-03-20 11:47:13 -04:00
Zach Pomerantz
089fba5dee test: parallelize cypress (again) with a consistent container (#6182)
* Revert "test: run cypress on one machine (#6181)"

This reverts commit a2812fcf79.

* test: use consistent container across cypress-test-matrix

* test: condition cypress-tests on cypress-test-matrix

* test: check cypress-test-matrix

* debug: cypress-test-matrix.result

* test: fix error

* test: fix error

* test: fix error

* test: fix error

* test: fix error

* test: fix error
2023-03-17 13:09:20 -07:00
cartcrom
503a33314f fix: use proper apollo client for ticks query (#6185)
fix: pass thegraph client to ticks query instead of default
2023-03-17 14:29:46 -04:00
Zach Pomerantz
6fee37c4b8 feat: trace jsonrpc (#6159)
* feat: maybeTrace

* feat: maybeTrace jsonrpc

* docs: maybeTrace

* test: fix test typing

* fix: pr feedback
2023-03-16 22:16:41 -07:00
Zach Pomerantz
a2812fcf79 test: run cypress on one machine (#6181)
* test: run cypress on one machine

* build: turn off parallel cypress
2023-03-16 21:10:36 -07:00
Vignesh Mohankumar
c42aeae96a fix: update useIsPoolsPage hook (#6179)
* fix: update `useIsPoolsPage` hook

* rename file
2023-03-16 19:36:07 -04:00
Vignesh Mohankumar
80ab000d34 fix: back button should lead to /pools not /pools/v2 (#6171) 2023-03-16 18:47:28 -04:00
eddie
81206f1eef test: widget integration tests (#6145)
* feat: upgrade widget

* test: swap widget integration tests

* test: handle conditional token safety warning

* fix: yarn dedup

* fix: try reformatting cy commands

* test: try waiting for page to load

* fix: update test

* fix: update test

* fix: test
2023-03-16 15:19:02 -07:00
Vignesh Mohankumar
568b05fda1 feat: support both /pool and /pools (#6173)
* more

* change locally
2023-03-16 18:08:10 -04:00
Vignesh Mohankumar
3d0ca21036 fix: remove Create a pool menu option (#6168) 2023-03-16 18:01:54 -04:00
Vignesh Mohankumar
8b743615d1 fix: link to providing liquidity help doc (#6169) 2023-03-16 18:01:46 -04:00
eddie
9e4fdabc34 feat: upgrade widget (#6176)
* feat: upgrade widget

* fix: yarn dedup
2023-03-16 14:05:52 -07:00
lynn
decb922d4b fix: handle undefined tax service banner counter in state (#6178)
* fix

* use ternary

* update
2023-03-16 16:42:43 -04:00
Vignesh Mohankumar
ac50555647 fix: equal width for CTA tiles on /pool (#6170) 2023-03-16 16:07:17 -04:00
Vignesh Mohankumar
f48356d0fb feat: link to token details from /pool/{} (#6162)
* feat: link to token details from /pool/{}

* use backend chain names

* use fn
2023-03-16 15:36:55 -04:00
Jack Short
a362f8797a chore: refactoring bag (#6039)
* chore: refactoring bag component to sub components (#6027)

* moving totalEthPrice to hook

* moving everything from bag to bag footer

* moving transaction state to sep hook

* explicit type

* itemsInBag

* fixing transaction tracking

* chore: refactor useFetchAssets to make it more readable (#6043)

* chore: refactoring useFetchAssets to make it more readable

* remvoing eslint stuff

* extracting what can be

* comments

* removing feature flag

* changing return type of useUsd hook

* zustand shallow
2023-03-16 14:39:50 -04:00
Jack Short
783f42abcc fix: cards floating point error (#6174) 2023-03-16 14:39:24 -04:00
Tina
0923cf4ac9 fix: Auto-slippage logic (#6167)
* range auto slippage from [.1% to 5%] and multiply gas by 110% instead of 10%

* remove newline

* no multiplier on gas, add comments

* consolidate constant variables
2023-03-16 12:46:59 -04:00
cartcrom
801958d0ae fix: skip when address undefined (#6172)
* fix: skip when address undefined

* fix: add back lowercase

* fix: use ms

* update loading var name
2023-03-16 09:33:47 -07:00
Zach Pomerantz
8392c29a1e feat: omit unnecessary sentry logs (#6166)
* fix: omit gracefully handled events from sentry

* fix: log locale load exception to sentry
2023-03-16 09:25:50 -07:00
Charles Bachmeier
8d36edf2b7 chore: remove no longer used anayltics query (#6165)
* chore: remove no longer used anayltics query

* Remove from index

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-03-15 18:30:28 -06:00
Charles Bachmeier
0f8d3fa506 fix: add more null checks for trending collections gql query (#6164)
add more null checks for trending collections gql query

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-03-15 18:30:01 -06:00
Zach Pomerantz
5f64149f39 feat: trace quote (#6160)
* feat: trace quote

* fix: include more data
2023-03-15 17:09:22 -07:00
Zach Pomerantz
7115729e3e feat: trace swap.send (#6147)
* feat: trace swap.send

* docs: comments

* test: sentry transaction trace

* fix: tag as non-widget

* fix: nits

* refactor: brackets

* fix: type TraceTags

* docs: traceTransaction

* chore: transaction->span

* docs: even more docs

* fix: is_widget
2023-03-15 16:39:04 -07:00
Zach Pomerantz
6618135e7d feat: enable sentry via .env.production (#6163)
* build: alphabetize

* fix: rm SENTRY_ENABLED as it defaults to false

* feat: enable sentry via .env.production

* test: do not send events from e2e tests

* fix: negation
2023-03-15 11:59:48 -07:00
65 changed files with 988 additions and 575 deletions

11
.env
View File

@@ -1,14 +1,13 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern.
ESLINT_NO_DEV_ERRORS=true
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_SENTRY_ENABLED=false
ESLINT_NO_DEV_ERRORS=true
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"

View File

@@ -1,14 +1,15 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern.
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink?platform=web"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_SENTRY_ENABLED=true
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
REACT_APP_SENTRY_ENABLED=false
REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd"

View File

@@ -64,20 +64,24 @@ jobs:
cypress-build:
runs-on: ubuntu-latest
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: cypress-cache
with:
path: /home/runner/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('node_modules/cypress') }}
path: /root/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('yarn.lock') }}
- if: steps.cypress-cache.outputs.cache-hit != 'true'
run: yarn cypress install
run: |
yarn cypress install
yarn cypress info
cypress-test-matrix:
needs: [build, cypress-build]
runs-on: ubuntu-latest
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
strategy:
fail-fast: false
matrix:
@@ -85,17 +89,15 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/download-artifact@v2
with:
name: build
path: build
- uses: actions/cache@v3
id: cypress-cache
with:
path: /home/runner/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('node_modules/cypress') }}
path: /root/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('yarn.lock') }}
- if: steps.cypress-cache.outputs.cache-hit != 'true'
run: yarn cypress install
@@ -111,9 +113,11 @@ jobs:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Included as a single job to check against for cypress test success, as cypress runs in a matrix.
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
cypress-tests:
if: ${{ always() }}
needs: [cypress-test-matrix]
runs-on: ubuntu-latest
steps:
- run: echo 'Finished cypress tests https\://dashboard.cypress.io/projects/yp82ef'
- if: needs.cypress-test-matrix.result != 'success'
run: exit 1

View File

@@ -43,7 +43,7 @@ For steps on local deployment, development, and code contribution, please see [C
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
- View V2 liquidity: <https://app.uniswap.org/#/pool/v2>
- View V2 liquidity: <https://app.uniswap.org/#/pools/v2>
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>

View File

@@ -26,6 +26,6 @@ describe('Landing Page', () => {
it('allows navigation to pool', () => {
cy.get(getTestSelector('pool-nav-link')).first().click()
cy.url().should('include', '/pool')
cy.url().should('include', '/pools')
})
})

View File

@@ -1,6 +1,6 @@
describe('Pool', () => {
beforeEach(() => {
cy.visit('/pool').then(() => {
cy.visit('/pools').then(() => {
cy.wait('@eth_blockNumber')
})
})

View File

@@ -0,0 +1,107 @@
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { getClassContainsSelector, getTestSelector } from '../utils'
const UNI_GOERLI = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
describe('swap widget integration tests', () => {
const verifyInputToken = (inputText: string) => {
cy.get(getClassContainsSelector('TokenButtonRow')).first().contains(inputText)
}
const verifyOutputToken = (outputText: string) => {
cy.get(getClassContainsSelector('TokenButtonRow')).last().contains(outputText)
}
const selectOutputAndSwitch = (outputText: string) => {
// open token selector...
cy.contains('Select token').click()
// select token...
cy.contains(outputText).click()
cy.get('body')
.then(($body) => {
if ($body.find(getTestSelector('TokenSafetyWrapper')).length) {
return 'I understand'
}
return 'You pay' // Just click on a random element as a no-op
})
.then((selector) => {
cy.contains(selector).click()
})
// token selector should close...
cy.contains('Search name or paste address').should('not.exist')
cy.get(getClassContainsSelector('ReverseButton')).first().click()
}
describe('widget on swap page', () => {
beforeEach(() => {
cy.viewport(1200, 800)
})
it('should have the correct default input/output and token selection should work', () => {
cy.visit('/swap', { featureFlags: [FeatureFlag.swapWidget] }).then(() => {
cy.wait('@eth_blockNumber')
verifyInputToken('ETH')
verifyOutputToken('Select token')
selectOutputAndSwitch('UNI')
verifyInputToken('UNI')
verifyOutputToken('ETH')
})
})
it('should have the correct default input from URL params ', () => {
cy.visit(`/swap?inputCurrency=${UNI_GOERLI}`, {
featureFlags: [FeatureFlag.swapWidget],
}).then(() => {
cy.wait('@eth_blockNumber')
})
verifyInputToken('UNI')
verifyOutputToken('Select token')
selectOutputAndSwitch('WETH')
verifyInputToken('WETH')
verifyOutputToken('UNI')
})
it('should have the correct default output from URL params ', () => {
cy.visit(`/swap?outputCurrency=${UNI_GOERLI}`, {
featureFlags: [FeatureFlag.swapWidget],
}).then(() => {
cy.wait('@eth_blockNumber')
})
verifyInputToken('Select token')
verifyOutputToken('UNI')
cy.get(getClassContainsSelector('ReverseButton')).first().click()
verifyInputToken('UNI')
verifyOutputToken('Select token')
selectOutputAndSwitch('WETH')
verifyInputToken('WETH')
verifyOutputToken('UNI')
})
})
describe('widget on Token Detail Page', () => {
beforeEach(() => {
cy.viewport(1200, 800)
cy.visit(`/tokens/ethereum/${UNI_GOERLI}`, { featureFlags: [FeatureFlag.swapWidget] }).then(() => {
cy.wait('@eth_blockNumber')
})
})
it('should have the expected output for a tokens detail page', () => {
verifyOutputToken('UNI')
cy.contains('Connect to Ethereum').should('exist')
})
})
})

View File

@@ -2,7 +2,7 @@ import { getTestSelector } from '../utils'
describe('Wallet Dropdown', () => {
before(() => {
cy.visit('/pool')
cy.visit('/pools')
})
it('should change the theme', () => {

View File

@@ -1,3 +1,5 @@
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
export const getTestSelectorStartsWith = (selectorId: string) => `[data-testid^=${selectorId}]`
export const getClassContainsSelector = (selectorId: string) => `[class*=${selectorId}]`

View File

@@ -154,7 +154,7 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "^2.47.3",
"@uniswap/widgets": "^2.48.5",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",

View File

@@ -149,7 +149,7 @@ export const AboutFooter = () => {
<TextLink to="/swap">Swap</TextLink>
<TextLink to="/tokens">Tokens</TextLink>
<TextLink to="/nfts">NFTs</TextLink>
<TextLink to="/pool">Pools</TextLink>
<TextLink to="/pools">Pools</TextLink>
</LinkGroup>
<LinkGroup>
<LinkGroupTitle>Protocol</LinkGroupTitle>

View File

@@ -50,7 +50,7 @@ export const MORE_CARDS = [
elementName: InterfaceElementName.ABOUT_PAGE_BUY_CRYPTO_CARD,
},
{
to: '/pool',
to: '/pools',
title: 'Earn',
description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.',
lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />,

View File

@@ -1,5 +1,4 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql'
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
@@ -218,12 +217,6 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.swapWidget}
label="Swap Widget"
/>
<FeatureFlagOption
variant={GqlRoutingVariant}
value={useGqlRoutingFlag()}
featureFlag={FeatureFlag.gqlRouting}
label="GraphQL NFT Routing"
/>
<FeatureFlagOption
variant={NftGraphqlVariant}
value={useNftGraphqlFlag()}

View File

@@ -1,5 +1,4 @@
import { Trans } from '@lingui/macro'
import * as Sentry from '@sentry/react'
import { Currency, Price, Token } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { AutoColumn, ColumnCenter } from 'components/Column'
@@ -156,10 +155,6 @@ export default function LiquidityChartRangeInput({
[isSorted, price, ticksAtLimit]
)
if (error) {
Sentry.captureMessage(error.toString(), 'log')
}
const isUninitialized = !currencyA || !currencyB || (formattedData === undefined && !isLoading)
return (

View File

@@ -3,7 +3,7 @@ import { useWeb3React } from '@web3-react/core'
import Web3Status from 'components/Web3Status'
import { chainIdToBackendName } from 'graphql/data/util'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useIsPoolPage } from 'hooks/useIsPoolPage'
import { useIsPoolsPage } from 'hooks/useIsPoolsPage'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { UniIcon } from 'nft/components/icons'
@@ -53,7 +53,7 @@ export const PageTabs = () => {
const { chainId: connectedChainId } = useWeb3React()
const chainName = chainIdToBackendName(connectedChainId)
const isPoolActive = useIsPoolPage()
const isPoolActive = useIsPoolsPage()
const isNftPage = useIsNftPage()
return (
@@ -67,8 +67,8 @@ export const PageTabs = () => {
<MenuItem dataTestId="nft-nav" href="/nfts" isActive={isNftPage}>
<Trans>NFTs</Trans>
</MenuItem>
<MenuItem href="/pool" dataTestId="pool-nav-link" isActive={isPoolActive}>
<Trans>Pool</Trans>
<MenuItem href="/pools" dataTestId="pool-nav-link" isActive={isPoolActive}>
<Trans>Pools</Trans>
</MenuItem>
</>
)

View File

@@ -75,8 +75,8 @@ export function AddRemoveTabs({
// detect if back should redirect to v3 or v2 pool page
const poolLink = location.pathname.includes('add/v2')
? '/pool/v2'
: '/pool' + (positionID ? `/${positionID.toString()}` : '')
? '/pools/v2'
: '/pools' + (positionID ? `/${positionID.toString()}` : '')
return (
<Tabs>

View File

@@ -210,7 +210,7 @@ export default function PositionListItem({
// check if price is within range
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
const positionSummaryLink = '/pool/' + tokenId
const positionSummaryLink = '/pools/' + tokenId
const removed = liquidity?.eq(0)

View File

@@ -122,7 +122,7 @@ export default function TaxServiceBanner() {
sessionStorage.setItem(TAX_SERVICE_DISMISSED, 'false')
}
const [bannerOpen, setBannerOpen] = useState(
sessionStorageTaxServiceDismissed !== 'true' && dismissals < MAX_RENDER_COUNT
sessionStorageTaxServiceDismissed !== 'true' && (dismissals === undefined || dismissals < MAX_RENDER_COUNT)
)
const onDismiss = useCallback(() => {
setModalOpen(false)
@@ -131,7 +131,7 @@ export default function TaxServiceBanner() {
const handleClose = useCallback(() => {
sessionStorage.setItem(TAX_SERVICE_DISMISSED, 'true')
setBannerOpen(false)
addTaxServiceDismissal(dismissals + 1)
dismissals === undefined ? addTaxServiceDismissal(1) : addTaxServiceDismissal(dismissals + 1)
}, [addTaxServiceDismissal, dismissals])
const handleLearnMoreClick = useCallback((e: any) => {

View File

@@ -258,7 +258,7 @@ export default function TokenSafety({
}
return displayWarning ? (
<Wrapper>
<Wrapper data-testid="TokenSafetyWrapper">
<Container>
<AutoColumn>
<LogoContainer>{logos}</LogoContainer>

View File

@@ -6,7 +6,7 @@ import TaxServiceBanner from 'components/TaxServiceModal/TaxServiceBanner'
import { useTaxServiceBannerEnabled } from 'featureFlags/flags/taxServiceBanner'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useIsPoolPage } from 'hooks/useIsPoolPage'
import { useIsPoolsPage } from 'hooks/useIsPoolsPage'
import { lazy } from 'react'
import { useLocation } from 'react-router-dom'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
@@ -27,7 +27,7 @@ export default function TopLevelModals() {
const { pathname } = useLocation()
const isNftPage = useIsNftPage()
const isPoolPage = useIsPoolPage()
const isPoolPage = useIsPoolsPage()
const isTaxModalServicePage = isNftPage || isPoolPage || pathname.startsWith('/swap')

View File

@@ -6,7 +6,6 @@ export enum FeatureFlag {
permit2 = 'permit2',
payWithAnyToken = 'payWithAnyToken',
swapWidget = 'swap_widget_replacement_enabled',
gqlRouting = 'gqlRouting',
statsigDummy = 'web_dummy_gate_amplitude_id',
nftGraphql = 'nft_graphql_migration',
taxService = 'tax_service_banner',

View File

@@ -1,7 +0,0 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useGqlRoutingFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.gqlRouting, BaseVariant.Enabled)
}
export { BaseVariant as GqlRoutingVariant }

View File

@@ -68,20 +68,20 @@ export function useTrendingCollections(size: number, timePeriod: HistoryDuration
const collection = edge?.node
return {
name: collection.name,
address: collection.nftContracts?.[0].address,
address: collection.nftContracts?.[0]?.address,
imageUrl: collection.image?.url,
bannerImageUrl: collection.bannerImage?.url,
isVerified: collection.isVerified,
volume: collection.markets?.[0].volume?.value,
volumeChange: collection.markets?.[0].volumePercentChange?.value,
floor: collection.markets?.[0].floorPrice?.value,
floorChange: collection.markets?.[0].floorPricePercentChange?.value,
marketCap: collection.markets?.[0].totalVolume?.value,
volume: collection.markets?.[0]?.volume?.value,
volumeChange: collection.markets?.[0]?.volumePercentChange?.value,
floor: collection.markets?.[0]?.floorPrice?.value,
floorChange: collection.markets?.[0]?.floorPricePercentChange?.value,
marketCap: collection.markets?.[0]?.totalVolume?.value,
percentListed:
(collection.markets?.[0].listings?.value ?? 0) / (collection.nftContracts?.[0].totalSupply ?? 1),
owners: collection.markets?.[0].owners,
sales: collection.markets?.[0].sales?.value,
totalSupply: collection.nftContracts?.[0].totalSupply,
(collection.markets?.[0]?.listings?.value ?? 0) / (collection.nftContracts?.[0]?.totalSupply ?? 1),
owners: collection.markets?.[0]?.owners,
sales: collection.markets?.[0]?.sales?.value,
totalSupply: collection.nftContracts?.[0]?.totalSupply,
}
}),
[data?.topCollections?.edges]

View File

@@ -1,12 +1,9 @@
import { useQuery } from '@apollo/client'
import gql from 'graphql-tag'
import { useMemo } from 'react'
import { AllV3TicksQuery } from './__generated__/types-and-hooks'
import { apolloClient } from './apollo'
const query = gql`
query AllV3Ticks($poolAddress: String!, $skip: Int!) {
gql`
query AllV3Ticks($poolAddress: String, $skip: Int!) {
ticks(first: 1000, skip: $skip, where: { poolAddress: $poolAddress }, orderBy: tickIdx) {
tick: tickIdx
liquidityNet
@@ -18,27 +15,3 @@ const query = gql`
export type Ticks = AllV3TicksQuery['ticks']
export type TickData = Ticks[number]
export default function useAllV3TicksQuery(poolAddress: string | undefined, skip: number, interval: number) {
const {
data,
loading: isLoading,
error,
} = useQuery<Record<'ticks', Ticks>>(query, {
variables: {
poolAddress: poolAddress?.toLowerCase(),
skip,
},
pollInterval: interval,
client: apolloClient,
})
return useMemo(
() => ({
error,
isLoading,
data,
}),
[data, error, isLoading]
)
}

View File

@@ -13,10 +13,7 @@ import { InterfaceTrade } from 'state/routing/types'
import useGasPrice from './useGasPrice'
import useStablecoinPrice, { useStablecoinValue } from './useStablecoinPrice'
const V3_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50%
const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10%
const DEFAULT_AUTO_SLIPPAGE = ONE_TENTHS_PERCENT
const GAS_ESTIMATE_BUFFER = new Percent(10, 100) // 10%
const DEFAULT_AUTO_SLIPPAGE = new Percent(1, 1000) // .10%
// Base costs regardless of how many hops in the route
const V3_SWAP_BASE_GAS_ESTIMATE = 100_000
@@ -67,8 +64,10 @@ function guesstimateGas(trade: Trade<Currency, Currency, TradeType> | undefined)
return undefined
}
const MIN_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 1000) // 0.5%
const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(25, 100) // 25%
const MIN_AUTO_SLIPPAGE_TOLERANCE = DEFAULT_AUTO_SLIPPAGE
// assuming normal gas speeds, most swaps complete within 3 blocks and
// there's rarely price movement >5% in that time period
const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 100) // 5%
/**
* Returns slippage tolerance based on values from current trade, gas estimates from api, and active network.
@@ -102,12 +101,12 @@ export default function useAutoSlippageTolerance(
// if not, use local heuristic
const dollarCostToUse =
chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && trade?.gasUseEstimateUSD
? trade.gasUseEstimateUSD.multiply(GAS_ESTIMATE_BUFFER)
: dollarGasCost?.multiply(GAS_ESTIMATE_BUFFER)
? trade.gasUseEstimateUSD
: dollarGasCost
if (outputDollarValue && dollarCostToUse) {
// the rationale is that a user will not want their trade to fail for a loss due to slippage that is less than
// the cost of the gas of the failed transaction
// optimize for highest possible slippage without getting MEV'd
// so set slippage % such that the difference between expected amount out and minimum amount out < gas fee to sandwich the trade
const fraction = dollarCostToUse.asFraction.divide(outputDollarValue.asFraction)
const result = new Percent(fraction.numerator, fraction.denominator)
if (result.greaterThan(MAX_AUTO_SLIPPAGE_TOLERANCE)) {
@@ -121,6 +120,6 @@ export default function useAutoSlippageTolerance(
return result
}
return V3_SWAP_DEFAULT_SLIPPAGE
return DEFAULT_AUTO_SLIPPAGE
}, [trade, onL2, nativeGasPrice, gasEstimate, nativeCurrency, nativeCurrencyPrice, chainId, outputDollarValue])
}

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/react'
import { Token } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import uriToHttp from 'lib/utils/uriToHttp'
@@ -39,11 +38,10 @@ async function getColorFromToken(token: Token): Promise<string | null> {
logoURI = URIForEthToken(address)
return await getColorFromUriPath(logoURI)
} catch (error) {
Sentry.captureMessage(error.toString())
console.warn(`Unable to load logoURI (${token.symbol}): ${logoURI}`)
return null
}
}
return null
}
async function getColorFromUriPath(uri: string): Promise<string | null> {

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/react'
import { Currency, Token } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import useBlockNumber from 'lib/hooks/useBlockNumber'
@@ -89,8 +88,7 @@ function usePoolTVL(token0: Token | undefined, token1: Token | undefined) {
}
if (latestBlock - (_meta?.block?.number ?? 0) > MAX_DATA_BLOCK_AGE) {
Sentry.captureMessage(`Graph stale (latest block: ${latestBlock})`, 'log')
console.log(`Graph stale (latest block: ${latestBlock})`)
return {
isLoading,
error,

View File

@@ -1,8 +1,9 @@
import { useLocation } from 'react-router-dom'
export function useIsPoolPage() {
export function useIsPoolsPage() {
const { pathname } = useLocation()
return (
pathname.startsWith('/pools') ||
pathname.startsWith('/pool') ||
pathname.startsWith('/add') ||
pathname.startsWith('/remove') ||

View File

@@ -3,7 +3,9 @@ import { FeeAmount, nearestUsableTick, Pool, TICK_SPACINGS, tickToPrice } from '
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc'
import useAllV3TicksQuery, { TickData, Ticks } from 'graphql/thegraph/AllV3TicksQuery'
import { useAllV3TicksQuery } from 'graphql/thegraph/__generated__/types-and-hooks'
import { TickData, Ticks } from 'graphql/thegraph/AllV3TicksQuery'
import { apolloClient } from 'graphql/thegraph/apollo'
import JSBI from 'jsbi'
import { useSingleContractMultipleData } from 'lib/hooks/multicall'
import ms from 'ms.macro'
@@ -155,7 +157,12 @@ function useTicksFromSubgraph(
)
: undefined
return useAllV3TicksQuery(poolAddress, skip, ms`30s`)
return useAllV3TicksQuery({
variables: { poolAddress: poolAddress?.toLowerCase(), skip },
skip: !poolAddress,
pollInterval: ms`30s`,
client: apolloClient,
})
}
const MAX_THE_GRAPH_TICK_FETCH_VALUE = 1000
@@ -175,12 +182,11 @@ function useAllV3Ticks(
const [skipNumber, setSkipNumber] = useState(0)
const [subgraphTickData, setSubgraphTickData] = useState<Ticks>([])
const { data, error, isLoading } = useTicksFromSubgraph(
useSubgraph ? currencyA : undefined,
currencyB,
feeAmount,
skipNumber
)
const {
data,
error,
loading: isLoading,
} = useTicksFromSubgraph(useSubgraph ? currencyA : undefined, currencyB, feeAmount, skipNumber)
useEffect(() => {
if (data?.ticks.length) {

View File

@@ -10,13 +10,31 @@ import { FeeOptions, toHex } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { useCallback } from 'react'
import { trace } from 'tracing'
import { calculateGasMargin } from 'utils/calculateGasMargin'
import isZero from 'utils/isZero'
import { swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMessage'
import { didUserReject, swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMessage'
import { PermitSignature } from './usePermitAllowance'
class InvalidSwapError extends Error {}
/** Thrown when gas estimation fails. This class of error usually requires an emulator to determine the root cause. */
class GasEstimationError extends Error {
constructor() {
super(t`Your swap is expected to fail.`)
}
}
/**
* Thrown when the user modifies the transaction in-wallet before submitting it.
* In-wallet calldata modification nullifies any safeguards (eg slippage) from the interface, so we recommend reverting them immediately.
*/
class ModifiedSwapError extends Error {
constructor() {
super(
t`Your swap was modified through your wallet. If this was a mistake, please cancel immediately or risk losing your funds.`
)
}
}
interface SwapOptions {
slippageTolerance: Percent
@@ -33,55 +51,70 @@ export function useUniversalRouterSwapCallback(
const { account, chainId, provider } = useWeb3React()
return useCallback(async (): Promise<TransactionResponse> => {
try {
if (!account) throw new Error('missing account')
if (!chainId) throw new Error('missing chainId')
if (!provider) throw new Error('missing provider')
if (!trade) throw new Error('missing trade')
return trace(
'swap.send',
async ({ setTraceData, setTraceStatus, setTraceError }) => {
try {
if (!account) throw new Error('missing account')
if (!chainId) throw new Error('missing chainId')
if (!provider) throw new Error('missing provider')
if (!trade) throw new Error('missing trade')
const { calldata: data, value } = SwapRouter.swapERC20CallParameters(trade, {
slippageTolerance: options.slippageTolerance,
deadlineOrPreviousBlockhash: options.deadline?.toString(),
inputTokenPermit: options.permit,
fee: options.feeOptions,
})
const tx = {
from: account,
to: UNIVERSAL_ROUTER_ADDRESS(chainId),
data,
// TODO: universal-router-sdk returns a non-hexlified value.
...(value && !isZero(value) ? { value: toHex(value) } : {}),
}
let gasEstimate: BigNumber
try {
gasEstimate = await provider.estimateGas(tx)
} catch (gasError) {
console.warn(gasError)
throw new Error('Your swap is expected to fail')
}
const gasLimit = calculateGasMargin(gasEstimate)
const response = await provider
.getSigner()
.sendTransaction({ ...tx, gasLimit })
.then((response) => {
sendAnalyticsEvent(
SwapEventName.SWAP_SIGNED,
formatSwapSignedAnalyticsEventProperties({ trade, fiatValues, txHash: response.hash })
)
if (tx.data !== response.data) {
sendAnalyticsEvent(SwapEventName.SWAP_MODIFIED_IN_WALLET, { txHash: response.hash })
throw new InvalidSwapError(
t`Your swap was modified through your wallet. If this was a mistake, please cancel immediately or risk losing your funds.`
)
setTraceData('slippageTolerance', options.slippageTolerance.toFixed(2))
const { calldata: data, value } = SwapRouter.swapERC20CallParameters(trade, {
slippageTolerance: options.slippageTolerance,
deadlineOrPreviousBlockhash: options.deadline?.toString(),
inputTokenPermit: options.permit,
fee: options.feeOptions,
})
const tx = {
from: account,
to: UNIVERSAL_ROUTER_ADDRESS(chainId),
data,
// TODO(https://github.com/Uniswap/universal-router-sdk/issues/113): universal-router-sdk returns a non-hexlified value.
...(value && !isZero(value) ? { value: toHex(value) } : {}),
}
let gasEstimate: BigNumber
try {
gasEstimate = await provider.estimateGas(tx)
} catch (gasError) {
setTraceStatus('failed_precondition')
setTraceError(gasError)
console.warn(gasError)
throw new GasEstimationError()
}
const gasLimit = calculateGasMargin(gasEstimate)
setTraceData('gasLimit', gasLimit.toNumber())
const response = await provider
.getSigner()
.sendTransaction({ ...tx, gasLimit })
.then((response) => {
sendAnalyticsEvent(
SwapEventName.SWAP_SIGNED,
formatSwapSignedAnalyticsEventProperties({ trade, fiatValues, txHash: response.hash })
)
if (tx.data !== response.data) {
sendAnalyticsEvent(SwapEventName.SWAP_MODIFIED_IN_WALLET, { txHash: response.hash })
throw new ModifiedSwapError()
}
return response
})
return response
})
return response
} catch (swapError: unknown) {
if (swapError instanceof InvalidSwapError) throw swapError
throw new Error(swapErrorToUserReadableMessage(swapError))
}
} catch (swapError: unknown) {
if (swapError instanceof ModifiedSwapError) throw swapError
// Cancellations are not failures, and must be accounted for as 'cancelled'.
if (didUserReject(swapError)) setTraceStatus('cancelled')
// GasEstimationErrors are already traced when they are thrown.
if (!(swapError instanceof GasEstimationError)) setTraceError(swapError)
throw new Error(swapErrorToUserReadableMessage(swapError))
}
},
{ tags: { is_widget: false } }
)
}, [
account,
chainId,

View File

@@ -84,7 +84,8 @@ export async function dynamicActivate(locale: SupportedLocale) {
// Bundlers will either export it as default or as a named export named default.
i18n.load(locale, catalog.messages || catalog.default.messages)
} catch (error) {
Sentry.captureMessage(error.toString())
console.error(error)
Sentry.captureException(new Error(`Unable to load locale (${locale})`))
}
i18n.activate(locale)
}

View File

@@ -1,38 +1,16 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
import { BagFooter } from 'nft/components/bag/BagFooter'
import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal'
import { Column } from 'nft/components/Flex'
import { Overlay } from 'nft/components/modals/Overlay'
import {
useBag,
useIsMobile,
useProfilePageState,
useSellAsset,
useSendTransaction,
useTransactionResponse,
} from 'nft/hooks'
import { useTokenInput } from 'nft/hooks/useTokenInput'
import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
import {
buildNftTradeInputFromBagItems,
buildSellObject,
formatAssetEventProperties,
recalculateBagUsingPooledAssets,
sortUpdatedAssets,
} from 'nft/utils'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQueryClient } from 'react-query'
import { useBag, useIsMobile, useProfilePageState, useSellAsset } from 'nft/hooks'
import { BagStatus, ProfilePageStateType } from 'nft/types'
import { formatAssetEventProperties, recalculateBagUsingPooledAssets } from 'nft/utils'
import { useCallback, useEffect, useMemo, useState } from 'react'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex'
import { shallow } from 'zustand/shallow'
@@ -120,8 +98,6 @@ const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
)
const Bag = () => {
const { account, provider } = useWeb3React()
const { resetSellAssets, sellAssets } = useSellAsset(
({ reset, sellAssets }) => ({
resetSellAssets: reset,
@@ -132,36 +108,16 @@ const Bag = () => {
const { setProfilePageState } = useProfilePageState(({ setProfilePageState }) => ({ setProfilePageState }))
const {
bagStatus,
setBagStatus,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
bagIsLocked,
setLocked,
reset,
setItemsInBag,
bagExpanded,
toggleBag,
setTotalEthPrice,
setBagExpanded,
} = useBag((state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }), shallow)
const { bagStatus, bagIsLocked, reset, bagExpanded, toggleBag, setBagExpanded } = useBag(
(state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }),
shallow
)
const { uncheckedItemsInBag } = useBag(({ itemsInBag }) => ({ uncheckedItemsInBag: itemsInBag }))
const isProfilePage = useIsNftProfilePage()
const isDetailsPage = useIsNftDetailsPage()
const isNFTPage = useIsNftPage()
const isMobile = useIsMobile()
const usingGqlRouting = useGqlRoutingFlag() === GqlRoutingVariant.Enabled
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
const transactionState = useSendTransaction((state) => state.state)
const setTransactionState = useSendTransaction((state) => state.setState)
const transactionStateRef = useRef(transactionState)
const [setTransactionResponse] = useTransactionResponse((state) => [state.setTransactionResponse])
const tokenTradeInput = useTokenInput((state) => state.tokenTradeInput)
const queryClient = useQueryClient()
const itemsInBag = useMemo(() => recalculateBagUsingPooledAssets(uncheckedItemsInBag), [uncheckedItemsInBag])
@@ -175,210 +131,14 @@ const Bag = () => {
}
}
const { totalEthPrice } = useMemo(() => {
const totalEthPrice = itemsInBag.reduce(
(total, item) =>
item.status !== BagItemStatus.UNAVAILABLE
? total.add(
BigNumber.from(
item.asset.updatedPriceInfo ? item.asset.updatedPriceInfo.ETHPrice : item.asset.priceInfo.ETHPrice
)
)
: total,
BigNumber.from(0)
)
return { totalEthPrice }
}, [itemsInBag])
const purchaseAssets = async (routingData: RouteResponse, purchasingWithErc20: boolean) => {
if (!provider || !routingData) return
const purchaseResponse = await sendTransaction(
provider?.getSigner(),
itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset),
routingData,
purchasingWithErc20
)
if (
purchaseResponse &&
(transactionStateRef.current === TxStateType.Success || transactionStateRef.current === TxStateType.Failed)
) {
setLocked(false)
setModalIsOpen(false)
setTransactionResponse(purchaseResponse)
setBagExpanded({ bagExpanded: false })
reset()
}
}
const handleCloseBag = useCallback(() => {
setBagExpanded({ bagExpanded: false, manualClose: true })
}, [setBagExpanded])
const [fetchGqlRoute] = useNftRouteLazyQuery()
const fetchAssets = async () => {
const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
const ethSellObject = buildSellObject(
itemsToBuy
.reduce((ethTotal, asset) => ethTotal.add(BigNumber.from(asset.priceInfo.ETHPrice)), BigNumber.from(0))
.toString()
)
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
!bagIsLocked && setLocked(true)
setBagStatus(BagStatus.FETCHING_ROUTE)
try {
if (usingGqlRouting) {
fetchGqlRoute({
variables: {
senderAddress: usingGqlRouting && account ? account : '',
nftTrades: usingGqlRouting ? buildNftTradeInputFromBagItems(itemsInBag) : [],
tokenTrades: tokenTradeInput ? tokenTradeInput : undefined,
},
onCompleted: (data) => {
if (!data.nftRoute || !data.nftRoute.route) {
setBagStatus(BagStatus.ADDING_TO_BAG)
setLocked(false)
return
}
const purchasingWithErc20 = !!tokenTradeInput
const { route, routeResponse } = buildRouteResponse(data.nftRoute, purchasingWithErc20)
const { hasPriceAdjustment, updatedAssets } = combineBuyItemsWithTxRoute(itemsToBuy, route)
const shouldRefetchCalldata = hasPriceAdjustment && purchasingWithErc20
const fetchedPriceChangedAssets = updatedAssets
.filter((asset) => asset.updatedPriceInfo)
.sort(sortUpdatedAssets)
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const fetchedUnchangedAssets = updatedAssets.filter(
(asset) => !asset.updatedPriceInfo && !asset.isUnavailable
)
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...fetchedUnchangedAssets.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
])
let shouldLock = false
if (hasAssets) {
if (!shouldReview) {
if (shouldRefetchCalldata) {
setBagStatus(BagStatus.CONFIRM_QUOTE)
} else {
purchaseAssets(routeResponse, purchasingWithErc20)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
shouldLock = true
}
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
setBagStatus(BagStatus.IN_REVIEW)
}
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
setLocked(shouldLock)
},
})
} else {
const routeData = await queryClient.fetchQuery(['assetsRoute', ethSellObject, itemsToBuy, account], () =>
fetchRoute({
toSell: [ethSellObject],
toBuy: itemsToBuy,
senderAddress: account ?? '',
})
)
const { updatedAssets } = combineBuyItemsWithTxRoute(itemsToBuy, routeData.route)
const fetchedPriceChangedAssets = updatedAssets
.filter((asset) => asset.updatedPriceInfo)
.sort(sortUpdatedAssets)
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const fetchedUnchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...fetchedUnchangedAssets.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
])
setLocked(false)
if (hasAssets) {
if (!shouldReview) {
purchaseAssets(routeData, false)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
setBagStatus(BagStatus.IN_REVIEW)
}
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
}
} catch (error) {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
}
useEffect(() => {
useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state))
}, [])
useEffect(() => {
if (bagIsLocked && !isModalOpen) setModalIsOpen(true)
}, [bagIsLocked, isModalOpen])
useEffect(() => {
if (transactionStateRef.current === TxStateType.Confirming) setBagStatus(BagStatus.PROCESSING_TRANSACTION)
if (transactionStateRef.current === TxStateType.Denied || transactionStateRef.current === TxStateType.Invalid) {
if (transactionStateRef.current === TxStateType.Invalid) setBagStatus(BagStatus.WARNING)
else setBagStatus(BagStatus.CONFIRM_REVIEW)
setTransactionState(TxStateType.New)
setLocked(false)
setModalIsOpen(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactionStateRef.current])
useEffect(() => {
setTotalEthPrice(totalEthPrice)
}, [totalEthPrice, setTotalEthPrice])
const hasAssetsToShow = itemsInBag.length > 0
const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => {
@@ -422,7 +182,7 @@ const Bag = () => {
{isProfilePage ? <ProfileBagContent /> : <BagContent />}
</Column>
{hasAssetsToShow && !isProfilePage && (
<BagFooter totalEthPrice={totalEthPrice} fetchAssets={fetchAssets} eventProperties={eventProperties} />
<BagFooter setModalIsOpen={setModalIsOpen} eventProperties={eventProperties} />
)}
{isSellingAssets && isProfilePage && (
<ContinueButton

View File

@@ -20,10 +20,13 @@ import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useBag } from 'nft/hooks/useBag'
import { useBagTotalEthPrice } from 'nft/hooks/useBagTotalEthPrice'
import useDerivedPayWithAnyTokenSwapInfo from 'nft/hooks/useDerivedPayWithAnyTokenSwapInfo'
import { useFetchAssets } from 'nft/hooks/useFetchAssets'
import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap'
import usePermit2Approval from 'nft/hooks/usePermit2Approval'
import { PriceImpact, usePriceImpact } from 'nft/hooks/usePriceImpact'
import { useSubscribeTransactionState } from 'nft/hooks/useSubscribeTransactionState'
import { useTokenInput } from 'nft/hooks/useTokenInput'
import { useWalletBalance } from 'nft/hooks/useWalletBalance'
import { BagStatus } from 'nft/types'
@@ -272,8 +275,7 @@ const FiatValue = ({
}
interface BagFooterProps {
totalEthPrice: BigNumber
fetchAssets: () => void
setModalIsOpen: (open: boolean) => void
eventProperties: Record<string, unknown>
}
@@ -284,11 +286,12 @@ const PENDING_BAG_STATUSES = [
BagStatus.PROCESSING_TRANSACTION,
]
export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFooterProps) => {
export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) => {
const toggleWalletModal = useToggleWalletModal()
const theme = useTheme()
const { account, chainId, connector } = useWeb3React()
const connected = Boolean(account && chainId)
const totalEthPrice = useBagTotalEthPrice()
const shouldUsePayWithAnyToken = usePayWithAnyTokenEnabled()
const inputCurrency = useTokenInput((state) => state.inputCurrency)
const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
@@ -297,7 +300,6 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
account ?? undefined,
!!inputCurrency && inputCurrency.isToken ? inputCurrency : undefined
)
const {
isLocked: bagIsLocked,
bagStatus,
@@ -312,13 +314,14 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
}),
shallow
)
const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false)
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
const activeCurrency = inputCurrency ?? defaultCurrency
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken && chainId === SupportedChainId.MAINNET
useSubscribeTransactionState(setModalIsOpen)
const fetchAssets = useFetchAssets()
const parsedOutputAmount = useMemo(() => {
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
}, [defaultCurrency, totalEthPrice])
@@ -374,7 +377,7 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
handleClick,
buttonColor,
} = useMemo(() => {
let handleClick = fetchAssets
let handleClick: (() => void) | (() => Promise<void>) = fetchAssets
let buttonText = <Trans>Something went wrong</Trans>
let disabled = true
let warningText = undefined

View File

@@ -2,14 +2,15 @@ import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { body, bodySmall } from 'nft/css/common.css'
import { useBag } from 'nft/hooks'
import { useBagTotalEthPrice, useBagTotalUsdPrice } from 'nft/hooks/useBagTotalEthPrice'
import { ethNumberStandardFormatter, formatWeiToDecimal, roundAndPluralize } from 'nft/utils'
import * as styles from './MobileHoverBag.css'
export const MobileHoverBag = () => {
const itemsInBag = useBag((state) => state.itemsInBag)
const toggleBag = useBag((state) => state.toggleBag)
const totalEthPrice = useBag((state) => state.totalEthPrice)
const totalUsdPrice = useBag((state) => state.totalUsdPrice)
const totalEthPrice = useBagTotalEthPrice()
const totalUsdPrice = useBagTotalUsdPrice()
const shouldShowBag = itemsInBag.length > 0
@@ -47,11 +48,10 @@ export const MobileHoverBag = () => {
{roundAndPluralize(itemsInBag.length, 'NFT')}
</Box>
<Row gap="8">
<Box className={body}>{`${formatWeiToDecimal(totalEthPrice.toString())}`}</Box>
<Box color="textSecondary" className={bodySmall}>{`${ethNumberStandardFormatter(
totalUsdPrice,
true
)}`}</Box>
<Box className={body}>{`${formatWeiToDecimal(totalEthPrice.toString())}`} ETH</Box>
<Box color="textSecondary" className={bodySmall}>
{ethNumberStandardFormatter(totalUsdPrice, true)}
</Box>
</Row>
</Column>
</Row>

View File

@@ -119,7 +119,7 @@ const StyledCardContainer = styled.div<{ selected: boolean; isDisabled: boolean
right: 0px;
bottom: 0px;
left: 0px;
border: ${({ selected, theme }) => (selected ? '3px' : !theme.darkMode ? '0px' : '1px')} solid;
border: ${({ selected }) => (selected ? '3px' : '1px')} solid;
border-radius: ${BORDER_RADIUS}px;
border-color: ${({ theme, selected }) => (selected ? theme.accentAction : theme.backgroundOutline)};
pointer-events: none;

View File

@@ -1,4 +1,3 @@
import { BigNumber } from '@ethersproject/bignumber'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
import { v4 as uuidv4 } from 'uuid'
@@ -12,10 +11,6 @@ interface BagState {
setBagStatus: (state: BagStatus) => void
itemsInBag: BagItem[]
setItemsInBag: (items: BagItem[]) => void
totalEthPrice: BigNumber
setTotalEthPrice: (totalEthPrice: BigNumber) => void
totalUsdPrice: number | undefined
setTotalUsdPrice: (totalUsdPrice: number | undefined) => void
addAssetsToBag: (asset: UpdatedGenieAsset[], fromSweep?: boolean) => void
removeAssetsFromBag: (assets: UpdatedGenieAsset[], fromSweep?: boolean) => void
markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void
@@ -72,16 +67,6 @@ export const useBag = create<BagState>()(
set(() => ({
itemsInBag: items,
})),
totalEthPrice: BigNumber.from(0),
setTotalEthPrice: (totalEthPrice) =>
set(() => ({
totalEthPrice,
})),
totalUsdPrice: undefined,
setTotalUsdPrice: (totalUsdPrice) =>
set(() => ({
totalUsdPrice,
})),
addAssetsToBag: (assets, fromSweep = false) =>
set(({ itemsInBag }) => {
if (get().isLocked) return { itemsInBag: get().itemsInBag }

View File

@@ -0,0 +1,44 @@
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { useCurrency } from 'hooks/Tokens'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { BagItemStatus } from 'nft/types'
import { useMemo } from 'react'
import { useBag } from './useBag'
export function useBagTotalEthPrice(): BigNumber {
const itemsInBag = useBag((state) => state.itemsInBag)
return useMemo(() => {
const totalEthPrice = itemsInBag.reduce(
(total, item) =>
item.status !== BagItemStatus.UNAVAILABLE
? total.add(
BigNumber.from(
item.asset.updatedPriceInfo ? item.asset.updatedPriceInfo.ETHPrice : item.asset.priceInfo.ETHPrice
)
)
: total,
BigNumber.from(0)
)
return totalEthPrice
}, [itemsInBag])
}
export function useBagTotalUsdPrice(): string | undefined {
const totalEthPrice = useBagTotalEthPrice()
const defaultCurrency = useCurrency('ETH')
const parsedOutputAmount = useMemo(() => {
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
}, [defaultCurrency, totalEthPrice])
const usdcValue = useStablecoinValue(parsedOutputAmount)
return useMemo(() => {
return usdcValue?.toExact()
}, [usdcValue])
}

View File

@@ -0,0 +1,102 @@
import { useWeb3React } from '@web3-react/core'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { BagStatus } from 'nft/types'
import { buildNftTradeInputFromBagItems, recalculateBagUsingPooledAssets } from 'nft/utils'
import { getNextBagState, getPurchasableAssets } from 'nft/utils/bag'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { useCallback, useMemo } from 'react'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
import { usePurchaseAssets } from './usePurchaseAssets'
import { useTokenInput } from './useTokenInput'
export function useFetchAssets(): () => Promise<void> {
const { account } = useWeb3React()
const {
itemsInBag: uncheckedItemsInBag,
setBagStatus,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
isLocked: bagIsLocked,
setLocked: setBagLocked,
setItemsInBag,
} = useBag(
({
itemsInBag,
setBagStatus,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
isLocked,
setLocked,
setItemsInBag,
}) => ({
itemsInBag,
setBagStatus,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
isLocked,
setLocked,
setItemsInBag,
}),
shallow
)
const tokenTradeInput = useTokenInput((state) => state.tokenTradeInput)
const itemsInBag = useMemo(() => recalculateBagUsingPooledAssets(uncheckedItemsInBag), [uncheckedItemsInBag])
const [fetchGqlRoute] = useNftRouteLazyQuery()
const purchaseAssets = usePurchaseAssets()
const resetStateBeforeFetch = useCallback(() => {
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
!bagIsLocked && setBagLocked(true)
setBagStatus(BagStatus.FETCHING_ROUTE)
}, [bagIsLocked, didOpenUnavailableAssets, setBagLocked, setBagStatus, setDidOpenUnavailableAssets])
return useCallback(async () => {
resetStateBeforeFetch()
fetchGqlRoute({
variables: {
senderAddress: account ? account : '',
nftTrades: buildNftTradeInputFromBagItems(itemsInBag),
tokenTrades: tokenTradeInput ? tokenTradeInput : undefined,
},
onCompleted: (data) => {
if (!data.nftRoute || !data.nftRoute.route) {
setBagStatus(BagStatus.ADDING_TO_BAG)
setBagLocked(false)
return
}
const wishAssetsToBuy = getPurchasableAssets(itemsInBag)
const purchasingWithErc20 = !!tokenTradeInput
const { route, routeResponse } = buildRouteResponse(data.nftRoute, purchasingWithErc20)
const { newBagItems, nextBagStatus } = getNextBagState(wishAssetsToBuy, route, purchasingWithErc20)
setItemsInBag(newBagItems)
setBagStatus(nextBagStatus)
if (nextBagStatus === BagStatus.CONFIRMING_IN_WALLET) {
purchaseAssets(routeResponse, wishAssetsToBuy, purchasingWithErc20)
setBagLocked(true)
return
}
setBagLocked(false)
},
})
}, [
account,
fetchGqlRoute,
itemsInBag,
purchaseAssets,
resetStateBeforeFetch,
setBagLocked,
setBagStatus,
setItemsInBag,
tokenTradeInput,
])
}

View File

@@ -0,0 +1,52 @@
import { useWeb3React } from '@web3-react/core'
import { RouteResponse, UpdatedGenieAsset } from 'nft/types'
import { useCallback } from 'react'
import shallow from 'zustand/shallow'
import { useBag } from './useBag'
import { useSendTransaction } from './useSendTransaction'
import { useTransactionResponse } from './useTransactionResponse'
export function usePurchaseAssets(): (
routingData: RouteResponse,
assetsToBuy: UpdatedGenieAsset[],
purchasingWithErc20?: boolean
) => Promise<void> {
const { provider } = useWeb3React()
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
const setTransactionResponse = useTransactionResponse((state) => state.setTransactionResponse)
const {
setLocked: setBagLocked,
setBagExpanded,
reset: resetBag,
} = useBag(
({ setLocked, setBagExpanded, reset }) => ({
setLocked,
setBagExpanded,
reset,
}),
shallow
)
return useCallback(
async (routingData: RouteResponse, assetsToBuy: UpdatedGenieAsset[], purchasingWithErc20 = false) => {
if (!provider) return
const purchaseResponse = await sendTransaction(
provider.getSigner(),
assetsToBuy,
routingData,
purchasingWithErc20
)
if (purchaseResponse) {
setBagLocked(false)
setTransactionResponse(purchaseResponse)
setBagExpanded({ bagExpanded: false })
resetBag()
}
},
[provider, resetBag, sendTransaction, setBagExpanded, setBagLocked, setTransactionResponse]
)
}

View File

@@ -12,7 +12,7 @@ import ERC721 from '../../abis/erc721.json'
import ERC1155 from '../../abis/erc1155.json'
import CryptoPunksMarket from '../abis/CryptoPunksMarket.json'
import { GenieAsset, RouteResponse, RoutingItem, TxResponse, TxStateType, UpdatedGenieAsset } from '../types'
import { combineBuyItemsWithTxRoute } from '../utils/txRoute/combineItemsWithTxRoute'
import { compareAssetsWithTransactionRoute } from '../utils/txRoute/combineItemsWithTxRoute'
interface TxState {
state: TxStateType
@@ -147,7 +147,7 @@ const findNFTsPurchased = (
)
})
return combineBuyItemsWithTxRoute(transferredItems, txRoute).updatedAssets
return compareAssetsWithTransactionRoute(transferredItems, txRoute).updatedAssets
}
const findNFTsNotPurchased = (toBuy: GenieAsset[], nftsPurchased: UpdatedGenieAsset[]) => {

View File

@@ -0,0 +1,38 @@
import { BagStatus, TxStateType } from 'nft/types'
import { useEffect, useRef } from 'react'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
import { useSendTransaction } from './useSendTransaction'
export function useSubscribeTransactionState(setModalIsOpen: (isOpen: boolean) => void) {
const transactionState = useSendTransaction((state) => state.state)
const setTransactionState = useSendTransaction((state) => state.setState)
const transactionStateRef = useRef(transactionState)
const { setBagStatus, setLocked: setBagLocked } = useBag(
({ setBagExpanded, setBagStatus, setLocked }) => ({
setBagExpanded,
setBagStatus,
setLocked,
}),
shallow
)
useEffect(() => {
useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state))
}, [])
useEffect(() => {
if (transactionStateRef.current === TxStateType.Confirming) setBagStatus(BagStatus.PROCESSING_TRANSACTION)
if (transactionStateRef.current === TxStateType.Denied || transactionStateRef.current === TxStateType.Invalid) {
if (transactionStateRef.current === TxStateType.Invalid) {
setBagStatus(BagStatus.WARNING)
} else setBagStatus(BagStatus.CONFIRM_REVIEW)
setTransactionState(TxStateType.New)
setBagLocked(false)
setModalIsOpen(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setBagLocked, setBagStatus, setModalIsOpen, setTransactionState, transactionStateRef.current])
}

View File

@@ -1,6 +1,5 @@
export * from './ActivityFetcher'
export * from './CollectionPreviewFetcher'
export * from './logListing'
export * from './RouteFetcher'
export * from './SearchCollectionsFetcher'
export * from './TrendingCollectionsFetcher'

View File

@@ -1,36 +0,0 @@
import { ListingMarket, ListingRow } from 'nft/types'
interface Listing extends ListingRow {
marketplaces: ListingMarket[]
}
export const logListing = async (listings: ListingRow[], userAddress: string): Promise<boolean> => {
const url = `${process.env.REACT_APP_TEMP_API_URL}/nft/logGenieList`
const listingsConsolidated: Listing[] = listings.map((el) => ({ ...el, marketplaces: [] }))
const marketplacesById: Record<string, ListingMarket[]> = {}
const listingsWithMarketsConsolidated = listingsConsolidated.reduce((uniqueListings, curr) => {
const key = `${curr.asset.asset_contract.address}-${curr.asset.tokenId}`
if (marketplacesById[key]) {
marketplacesById[key].push(curr.marketplace)
} else {
marketplacesById[key] = [curr.marketplace]
}
if (!uniqueListings.some((listing) => `${listing.asset.asset_contract.address}-${listing.asset.tokenId}` === key)) {
curr.marketplaces = marketplacesById[key]
uniqueListings.push(curr)
}
return uniqueListings
}, [] as Listing[])
const payload = {
listings: listingsWithMarketsConsolidated,
userAddress,
}
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
return r.status === 200
}

75
src/nft/utils/bag.ts Normal file
View File

@@ -0,0 +1,75 @@
import { BagItem, BagItemStatus, BagStatus, RoutingItem, UpdatedGenieAsset } from 'nft/types'
import { compareAssetsWithTransactionRoute } from './txRoute/combineItemsWithTxRoute'
import { filterUpdatedAssetsByState } from './updatedAssets'
export function getPurchasableAssets(itemsInBag: BagItem[]): UpdatedGenieAsset[] {
return itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
}
function createBagFromUpdatedAssets(
unavailable: UpdatedGenieAsset[],
priceChanged: UpdatedGenieAsset[],
unchanged: UpdatedGenieAsset[]
): BagItem[] {
return [
...unavailable.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...priceChanged.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...unchanged.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
]
}
function evaluateNextBagState(
hasAssets: boolean,
shouldReview: boolean,
hasAssetsInReview: boolean,
shouldRefetchCalldata: boolean
): BagStatus {
if (!hasAssets) {
return BagStatus.ADDING_TO_BAG
}
if (shouldReview) {
if (hasAssetsInReview) {
return BagStatus.IN_REVIEW
}
return BagStatus.CONFIRM_REVIEW
}
if (shouldRefetchCalldata) {
return BagStatus.CONFIRM_QUOTE
}
return BagStatus.CONFIRMING_IN_WALLET
}
export function getNextBagState(
wishAssetsToBuy: UpdatedGenieAsset[],
route: RoutingItem[],
purchasingWithErc20: boolean
): { newBagItems: BagItem[]; nextBagStatus: BagStatus } {
const { hasPriceAdjustment, updatedAssets } = compareAssetsWithTransactionRoute(wishAssetsToBuy, route)
const shouldRefetchCalldata = hasPriceAdjustment && purchasingWithErc20
const { unchanged, priceChanged, unavailable } = filterUpdatedAssetsByState(updatedAssets)
const hasAssets = updatedAssets.length > 0
const hasAssetsInReview = priceChanged.length > 0
const hasUnavailableAssets = unavailable.length > 0
const shouldReview = hasAssetsInReview || hasUnavailableAssets
const newBagItems = createBagFromUpdatedAssets(unavailable, priceChanged, unchanged)
const nextBagStatus = evaluateNextBagState(hasAssets, shouldReview, hasAssetsInReview, shouldRefetchCalldata)
return { newBagItems, nextBagStatus }
}

View File

@@ -74,7 +74,7 @@ const itemInRouteAndSamePool = (
)
}
export const combineBuyItemsWithTxRoute = (
export const compareAssetsWithTransactionRoute = (
items: UpdatedGenieAsset[],
txRoute?: RoutingItem[]
): { hasPriceAdjustment: boolean; updatedAssets: UpdatedGenieAsset[] } => {

View File

@@ -20,3 +20,15 @@ export const getTotalNftValue = (nfts: UpdatedGenieAsset[]): BigNumber => {
)
)
}
export function filterUpdatedAssetsByState(assets: UpdatedGenieAsset[]): {
unchanged: UpdatedGenieAsset[]
priceChanged: UpdatedGenieAsset[]
unavailable: UpdatedGenieAsset[]
} {
const unchanged = assets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const priceChanged = assets.filter((asset) => asset.updatedPriceInfo).sort(sortUpdatedAssets)
const unavailable = assets.filter((asset) => asset.isUnavailable)
return { unchanged, priceChanged, unavailable }
}

View File

@@ -372,7 +372,7 @@ export default function AddLiquidity() {
if (txHash) {
onFieldAInput('')
// dont jump to pool page if creating
navigate('/pool')
navigate('/pools')
}
setTxHash('')
}, [navigate, onFieldAInput, txHash])

View File

@@ -97,6 +97,7 @@ function getCurrentPageFromLocation(locationPathname: string): InterfacePageName
return InterfacePageName.SWAP_PAGE
case locationPathname.startsWith('/vote'):
return InterfacePageName.VOTE_PAGE
case locationPathname.startsWith('/pools'):
case locationPathname.startsWith('/pool'):
return InterfacePageName.POOL_PAGE
case locationPathname.startsWith('/tokens'):
@@ -237,6 +238,11 @@ export default function App() {
<Route path="pool" element={<Pool />} />
<Route path="pool/:tokenId" element={<PositionPage />} />
<Route path="pools/v2/find" element={<PoolFinder />} />
<Route path="pools/v2" element={<PoolV2 />} />
<Route path="pools" element={<Pool />} />
<Route path="pools/:tokenId" element={<PositionPage />} />
<Route path="add/v2" element={<RedirectDuplicateTokenIdsV2 />}>
<Route path=":currencyIdA" />
<Route path=":currencyIdA/:currencyIdB" />

View File

@@ -116,7 +116,7 @@ export default function MigrateV2() {
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<BackArrow to="/pool" />
<BackArrow to="/pools" />
<ThemedText.DeprecatedMediumHeader>
<Trans>Migrate V2 Liquidity</Trans>
</ThemedText.DeprecatedMediumHeader>
@@ -173,7 +173,7 @@ export default function MigrateV2() {
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
<Trans>
Dont see one of your v2 positions?{' '}
<StyledInternalLink id="import-pool-link" to="/pool/v2/find">
<StyledInternalLink id="import-pool-link" to="/pools/v2/find">
Import it.
</StyledInternalLink>
</Trans>

View File

@@ -9,7 +9,7 @@ import { ExternalLink } from '../../theme'
const CTASection = styled.section`
display: grid;
grid-template-columns: 2fr 1.5fr;
grid-template-columns: 1fr 1fr;
gap: 8px;
opacity: 0.8;

View File

@@ -6,6 +6,7 @@ import { InterfacePageName } from '@uniswap/analytics-events'
import { formatPrice, NumberType } from '@uniswap/conedison/format'
import { Currency, CurrencyAmount, Fraction, Percent, Price, Token } from '@uniswap/sdk-core'
import { NonfungiblePositionManager, Pool, Position } from '@uniswap/v3-sdk'
import { SupportedChainId } from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { sendEvent } from 'components/analytics'
import Badge from 'components/Badge'
@@ -19,6 +20,7 @@ import { RowBetween, RowFixed } from 'components/Row'
import { Dots } from 'components/swap/styleds'
import Toggle from 'components/Toggle'
import TransactionConfirmationModal, { ConfirmationModalContent } from 'components/TransactionConfirmationModal'
import { CHAIN_ID_TO_BACKEND_NAME, isGqlSupportedChain } from 'graphql/data/util'
import { useToken } from 'hooks/Tokens'
import { useV3NFTPositionManagerContract } from 'hooks/useContract'
import useIsTickAtLimit from 'hooks/useIsTickAtLimit'
@@ -50,6 +52,15 @@ import { calculateGasMargin } from '../../utils/calculateGasMargin'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { LoadingRows } from './styleds'
const getTokenLink = (chainId: SupportedChainId, address: string) => {
if (isGqlSupportedChain(chainId)) {
const chainName = CHAIN_ID_TO_BACKEND_NAME[chainId]
return `${window.location.origin}/#/tokens/${chainName}/${address}`
} else {
return getExplorerLink(chainId, address, ExplorerDataType.TOKEN)
}
}
const PageWrapper = styled.div`
padding: 68px 16px 16px 16px;
@@ -196,7 +207,7 @@ function LinkedCurrency({ chainId, currency }: { chainId?: number; currency?: Cu
if (typeof chainId === 'number' && address) {
return (
<ExternalLink href={getExplorerLink(chainId, address, ExplorerDataType.TOKEN)}>
<ExternalLink href={getTokenLink(chainId, address)}>
<RowFixed>
<CurrencyLogo currency={currency} size="20px" style={{ marginRight: '0.5rem' }} />
<ThemedText.DeprecatedMain>{currency?.symbol} </ThemedText.DeprecatedMain>
@@ -605,7 +616,7 @@ export function PositionPage() {
<Link
data-cy="visit-pool"
style={{ textDecoration: 'none', width: 'fit-content', marginBottom: '0.5rem' }}
to="/pool"
to="/pools"
>
<HoverText>
<Trans> Back to Pools</Trans>

View File

@@ -11,7 +11,7 @@ import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import { isSupportedChain } from 'constants/chains'
import { useV3Positions } from 'hooks/useV3Positions'
import { useMemo } from 'react'
import { AlertTriangle, BookOpen, ChevronDown, ChevronsRight, Inbox, Layers, PlusCircle } from 'react-feather'
import { AlertTriangle, BookOpen, ChevronDown, ChevronsRight, Inbox, Layers } from 'react-feather'
import { Link } from 'react-router-dom'
import { useToggleWalletModal } from 'state/application/hooks'
import { useUserHideClosedPositions } from 'state/user/hooks'
@@ -224,16 +224,6 @@ export default function Pool() {
const showV2Features = Boolean(V2_FACTORY_ADDRESSES[chainId])
const menuItems = [
{
content: (
<PoolMenuItem>
<Trans>Create a pool</Trans>
<PlusCircle size={16} />
</PoolMenuItem>
),
link: '/add/ETH',
external: false,
},
{
content: (
<PoolMenuItem>
@@ -251,7 +241,7 @@ export default function Pool() {
<Layers size={16} />
</PoolMenuItem>
),
link: '/pool/v2',
link: '/pools/v2',
external: false,
},
{
@@ -261,7 +251,7 @@ export default function Pool() {
<BookOpen size={16} />
</PoolMenuItem>
),
link: 'https://docs.uniswap.org/',
link: 'https://support.uniswap.org/hc/en-us/categories/8122334631437-Providing-Liquidity-',
external: true,
},
]

View File

@@ -199,7 +199,7 @@ export default function Pool() {
<ResponsiveButtonSecondary as={Link} padding="6px 8px" to="/add/v2/ETH">
<Trans>Create a pair</Trans>
</ResponsiveButtonSecondary>
<ResponsiveButtonPrimary id="find-pool-button" as={Link} to="/pool/v2/find" padding="6px 8px">
<ResponsiveButtonPrimary id="find-pool-button" as={Link} to="/pools/v2/find" padding="6px 8px">
<Text fontWeight={500} fontSize={16}>
<Trans>Import Pool</Trans>
</Text>

View File

@@ -100,7 +100,7 @@ export default function PoolFinder() {
<Trace page={InterfacePageName.POOL_PAGE} shouldLogImpression>
<>
<AppBody>
<FindPoolTabs origin={query.get('origin') ?? '/pool/v2'} />
<FindPoolTabs origin={query.get('origin') ?? '/pools'} />
<AutoColumn style={{ padding: '1rem' }} gap="md">
<BlueCard>
<AutoColumn gap="10px">
@@ -162,7 +162,7 @@ export default function PoolFinder() {
<Text textAlign="center" fontWeight={500}>
<Trans>Pool Found!</Trans>
</Text>
<StyledInternalLink to="/pool/v2">
<StyledInternalLink to="pools/v2">
<Text textAlign="center">
<Trans>Manage this pool.</Trans>
</Text>

View File

@@ -55,7 +55,7 @@ export default function RemoveLiquidityV3() {
}, [tokenId])
if (parsedTokenId === null || parsedTokenId.eq(0)) {
return <Navigate to={{ ...location, pathname: '/pool' }} replace />
return <Navigate to={{ ...location, pathname: '/pools' }} replace />
}
return <Remove tokenId={parsedTokenId} />

View File

@@ -1,3 +1,4 @@
import { isAppUniswapOrg } from 'utils/env'
import { RouteHandlerCallbackOptions, RouteMatchCallbackOptions } from 'workbox-core'
import { getCacheKeyForURL, matchPrecache } from 'workbox-precaching'
import { Route } from 'workbox-routing'
@@ -24,7 +25,7 @@ export function matchDocument({ request, url }: RouteMatchCallbackOptions) {
// If this isn't app.uniswap.org (or a local build), skip.
// IPFS gateways may not have domain separation, so they cannot use document caching.
if (url.hostname !== 'app.uniswap.org' && !isDevelopment()) {
if (!isAppUniswapOrg(url) && !isDevelopment()) {
return false
}

View File

@@ -5,6 +5,7 @@ import { RPC_PROVIDERS } from 'constants/providers'
import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import ms from 'ms.macro'
import qs from 'qs'
import { trace } from 'tracing'
import { GetQuoteResult } from './types'
@@ -64,34 +65,59 @@ const PRICE_PARAMS = {
distributionPercent: 100,
}
interface GetQuoteArgs {
tokenInAddress: string
tokenInChainId: ChainId
tokenInDecimals: number
tokenInSymbol?: string
tokenOutAddress: string
tokenOutChainId: ChainId
tokenOutDecimals: number
tokenOutSymbol?: string
amount: string
routerPreference: RouterPreference
type: 'exactIn' | 'exactOut'
}
export const routingApi = createApi({
reducerPath: 'routingApi',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.uniswap.org/v1/',
}),
endpoints: (build) => ({
getQuote: build.query<
GetQuoteResult,
{
tokenInAddress: string
tokenInChainId: ChainId
tokenInDecimals: number
tokenInSymbol?: string
tokenOutAddress: string
tokenOutChainId: ChainId
tokenOutDecimals: number
tokenOutSymbol?: string
amount: string
routerPreference: RouterPreference
type: 'exactIn' | 'exactOut'
}
>({
getQuote: build.query<GetQuoteResult, GetQuoteArgs>({
async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) {
trace(
'quote',
async ({ setTraceError, setTraceStatus }) => {
try {
await queryFulfilled
} catch (error: unknown) {
if (error && typeof error === 'object' && 'error' in error) {
const queryError = (error as Record<'error', FetchBaseQueryError>).error
if (typeof queryError.status === 'number') {
setTraceStatus(queryError.status)
}
setTraceError(queryError)
} else {
throw error
}
}
},
{
data: {
...args,
isPrice: args.routerPreference === RouterPreference.PRICE,
isAutoRouter: args.routerPreference === RouterPreference.API,
},
tags: { is_widget: false },
}
)
},
async queryFn(args, _api, _extraOptions, fetch) {
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, routerPreference, type } =
args
let result
try {
if (routerPreference === RouterPreference.API) {
const query = qs.stringify({
@@ -103,10 +129,10 @@ export const routingApi = createApi({
amount,
type,
})
result = await fetch(`quote?${query}`)
return (await fetch(`quote?${query}`)) as { data: GetQuoteResult } | { error: FetchBaseQueryError }
} else {
const router = getRouter(args.tokenInChainId)
result = await getClientSideQuote(
return await getClientSideQuote(
args,
router,
// TODO(zzmp): Use PRICE_PARAMS for RouterPreference.PRICE.
@@ -114,12 +140,10 @@ export const routingApi = createApi({
CLIENT_PARAMS
)
}
return { data: result.data as GetQuoteResult }
} catch (e) {
} catch (error) {
// TODO: fall back to client-side quoter when auto router fails.
// deprecate 'legacy' v2/v3 routers first.
return { error: e as FetchBaseQueryError }
return { error: { status: 'CUSTOM_ERROR', error: error.toString(), data: error } }
}
},
keepUnusedDataFor: ms`10s`,

View File

@@ -93,7 +93,7 @@ export function useIsExpertMode(): boolean {
return useAppSelector((state) => state.user.userExpertMode)
}
export function useTaxServiceDismissal(): [number, (dismissals: number) => void] {
export function useTaxServiceDismissal(): [number | undefined, (dismissals: number) => void] {
const dispatch = useAppDispatch()
const taxServiceDismissals = useAppSelector((state) => state.user.taxServiceDismissals)
const setDismissals = useCallback(

View File

@@ -9,7 +9,7 @@ import { SerializedPair, SerializedToken } from './types'
const currentTimestamp = () => new Date().getTime()
export interface UserState {
taxServiceDismissals: number
taxServiceDismissals: number | undefined
selectedWallet?: ConnectionType

View File

@@ -7,6 +7,8 @@ import { SharedEventName } from '@uniswap/analytics-events'
import { isSentryEnabled } from 'utils/env'
import { getEnvName, isProductionEnv } from 'utils/env'
export { trace } from './trace'
// Dump some metadata into the window to allow client verification.
window.GIT_COMMIT_HASH = process.env.REACT_APP_GIT_COMMIT_HASH

144
src/tracing/trace.test.ts Normal file
View File

@@ -0,0 +1,144 @@
import '@sentry/tracing' // required to populate Sentry.startTransaction, which is not included in the core module
import * as Sentry from '@sentry/react'
import { Transaction } from '@sentry/tracing'
import assert from 'assert'
import { trace } from './trace'
jest.mock('@sentry/react', () => {
return {
startTransaction: jest.fn(),
}
})
const startTransaction = Sentry.startTransaction as jest.Mock
function getTransaction(index = 0): Transaction {
const transactions = startTransaction.mock.results.map(({ value }) => value)
expect(transactions).toHaveLength(index + 1)
const transaction = transactions[index]
expect(transaction).toBeDefined()
return transaction
}
describe('trace', () => {
beforeEach(() => {
const Sentry = jest.requireActual('@sentry/react')
startTransaction.mockReset().mockImplementation((context) => {
const transaction: Transaction = Sentry.startTransaction(context)
transaction.initSpanRecorder()
return transaction
})
})
it('propagates callback', async () => {
await expect(trace('test', () => Promise.resolve('resolved'))).resolves.toBe('resolved')
await expect(trace('test', () => Promise.reject('rejected'))).rejects.toBe('rejected')
})
it('records transaction', async () => {
const metadata = { data: { a: 'a', b: 2 }, tags: { is_widget: true } }
await trace('test', () => Promise.resolve(), metadata)
const transaction = getTransaction()
expect(transaction.name).toBe('test')
expect(transaction.data).toEqual({ a: 'a', b: 2 })
expect(transaction.tags).toEqual({ is_widget: true })
})
describe('defaults status', () => {
it('"ok" if resolved', async () => {
await trace('test', () => Promise.resolve())
const transaction = getTransaction()
expect(transaction.status).toBe('ok')
})
it('"internal_error" if rejected, with data.error set to rejection', async () => {
const error = new Error('Test error')
await expect(trace('test', () => Promise.reject(error))).rejects.toBe(error)
const transaction = getTransaction()
expect(transaction.status).toBe('internal_error')
expect(transaction.data).toEqual({ error })
})
})
describe('setTraceData', () => {
it('sets transaction data', async () => {
await trace('test', ({ setTraceData }) => {
setTraceData('a', 'a')
setTraceData('b', 2)
return Promise.resolve()
})
const transaction = getTransaction()
expect(transaction.data).toEqual({ a: 'a', b: 2 })
})
})
describe('setTraceTag', () => {
it('sets a transaction tag', async () => {
await trace('test', ({ setTraceTag }) => {
setTraceTag('is_widget', true)
return Promise.resolve()
})
const transaction = getTransaction()
expect(transaction.tags).toEqual({ is_widget: true })
})
})
describe('setTraceStatus', () => {
it('sets a transaction status with a string', async () => {
await trace('test', ({ setTraceStatus }) => {
setTraceStatus('cancelled')
return Promise.resolve()
})
let transaction = getTransaction(0)
expect(transaction.status).toBe('cancelled')
await expect(
trace('test', ({ setTraceStatus }) => {
setTraceStatus('failed_precondition')
return Promise.reject()
})
).rejects.toBeUndefined()
transaction = getTransaction(1)
expect(transaction.status).toBe('failed_precondition')
})
it('sets a transaction http status with a number', async () => {
await trace('test', ({ setTraceStatus }) => {
setTraceStatus(429)
return Promise.resolve()
})
const transaction = getTransaction()
expect(transaction.status).toBe('resource_exhausted')
})
})
describe('setTraceError', () => {
it('sets transaction data.error', async () => {
const error = new Error('Test error')
await expect(
trace('test', ({ setTraceError }) => {
setTraceError(error)
return Promise.reject(new Error(`Wrapped ${error.message}`))
})
).rejects.toBeDefined()
const transaction = getTransaction()
expect(transaction.data).toEqual({ error })
})
})
describe('traceChild', () => {
it('starts a span under a transaction', async () => {
await trace('test', ({ traceChild }) => {
traceChild('child', () => Promise.resolve(), { data: { e: 'e' }, tags: { is_widget: true } })
return Promise.resolve()
})
const transaction = getTransaction()
const span = transaction.spanRecorder?.spans[1]
assert(span)
expect(span.op).toBe('child')
expect(span.data).toEqual({ e: 'e' })
expect(span.tags).toEqual({ is_widget: true })
})
})
})

89
src/tracing/trace.ts Normal file
View File

@@ -0,0 +1,89 @@
import * as Sentry from '@sentry/react'
import { Span, SpanStatusType } from '@sentry/tracing'
type TraceTags = {
is_widget: boolean
}
interface TraceMetadata {
/** Arbitrary data stored on a trace. */
data?: Record<string, unknown>
/** Indexed (ie searchable) tags associated with a trace. */
tags?: Partial<TraceTags>
}
// These methods are provided as an abstraction so that users will not interact with Sentry directly.
// This avoids tightly coupling Sentry to our instrumentation outside of this file, in case we swap services.
interface TraceCallbackOptions {
/**
* Traces the callback as a child of the active trace.
* @param name - The name of the child. (On Sentry, this will appear as the "op".)
* @param callback - The callback to trace. The child trace will run for the duration of the callback.
* @param metadata - Any data or tags to include in the child trace.
*/
traceChild<T>(name: string, callback: TraceCallback<T>, metadata?: TraceMetadata): Promise<T>
setTraceData(key: string, value: unknown): void
setTraceTag<K extends keyof TraceTags>(key: K, value: TraceTags[K]): void
/**
* Sets the status of a trace. If unset, the status will be set to 'ok' (or 'internal_error' if the callback throws).
* @param status - If a number is passed, the corresponding http status will be used.
*/
setTraceStatus(status: number | SpanStatusType): void
/** Sets the error data of a trace. If unset and the callback throws, the thrown error will be set. */
setTraceError(error: unknown): void
}
type TraceCallback<T> = (options: TraceCallbackOptions) => Promise<T>
/**
* Sets up TraceCallbackOptions for a Span (NB: Transaction extends Span).
* @returns a handler which will run a TraceCallback and propagate its result.
*/
function traceSpan(span?: Span) {
const traceChild = <T>(name: string, callback: TraceCallback<T>, metadata?: TraceMetadata) => {
const child = span?.startChild({ ...metadata, op: name })
return traceSpan(child)(callback)
}
const setTraceData = <K extends keyof TraceTags>(key: K, value: TraceTags[K]) => {
span?.setData(key, value)
}
const setTraceTag = (key: string, value: string | number | boolean) => {
span?.setTag(key, value)
}
const setTraceStatus = (status: number | SpanStatusType) => {
if (typeof status === 'number') {
span?.setHttpStatus(status)
} else {
span?.setStatus(status)
}
}
const setTraceError = (error: unknown) => {
span?.setData('error', error)
}
return async function boundTrace<T>(callback: TraceCallback<T>): Promise<T> {
try {
return await callback({ traceChild, setTraceData, setTraceTag, setTraceStatus, setTraceError })
} catch (error) {
// Do not overwrite any custom status or error data that was already set.
if (!span?.status) span?.setStatus('internal_error')
if (!span?.data.error) span?.setData('error', error)
throw error
} finally {
// If no status was reported, assume that it was 'ok'. Otherwise, it will default to 'unknown'.
if (!span?.status) span?.setStatus('ok')
span?.finish()
}
}
}
/**
* Traces the callback, adding any metadata to the trace.
* @param name - The name of your trace.
* @param callback - The callback to trace. The trace will run for the duration of the callback.
* @param metadata - Any data or tags to include in the trace.
*/
export async function trace<T>(name: string, callback: TraceCallback<T>, metadata?: TraceMetadata): Promise<T> {
const transaction = Sentry.startTransaction({ name, data: metadata?.data, tags: metadata?.tags })
return traceSpan(transaction)(callback)
}

View File

@@ -7,7 +7,7 @@ export function isTestEnv(): boolean {
}
export function isStagingEnv(): boolean {
// NB: This is set in vercel builds.
// This is set in vercel builds.
return Boolean(process.env.REACT_APP_STAGING)
}
@@ -15,7 +15,13 @@ export function isProductionEnv(): boolean {
return process.env.NODE_ENV === 'production' && !isStagingEnv()
}
export function isAppUniswapOrg({ hostname }: { hostname: string }): boolean {
return hostname === 'app.uniswap.org'
}
export function isSentryEnabled(): boolean {
// Disable in e2e test environments
if (isStagingEnv() || (isProductionEnv() && !isAppUniswapOrg(window.location))) return false
return process.env.REACT_APP_SENTRY_ENABLED === 'true'
}

View File

@@ -1,30 +1,18 @@
// eslint-disable-next-line no-restricted-imports
import { t } from '@lingui/macro'
/**
* This is hacking out the revert reason from the ethers provider thrown error however it can.
* This object seems to be undocumented by ethers.
* @param error an error from the ethers provider
*/
export function swapErrorToUserReadableMessage(error: any): string {
function getReason(error: any): string | undefined {
let reason: string | undefined
if (error.code) {
switch (error.code) {
case 4001:
return t`Transaction rejected`
}
}
console.warn('Swap error:', error)
while (error) {
reason = error.reason ?? error.message ?? reason
error = error.error ?? error.data?.originalError
}
return reason
}
// The 4001 error code doesn't capture the case where users reject a transaction for all wallets,
// so we need to parse the reason for these special cases:
export function didUserReject(error: any): boolean {
const reason = getReason(error)
if (
error?.code === 4001 ||
// ethers v5.7.0 wrapped error
error?.code === 'ACTION_REJECTED' ||
// For Rainbow :
@@ -32,20 +20,35 @@ export function swapErrorToUserReadableMessage(error: any): string {
// For Frame:
reason?.match(/declined/i) ||
// For SafePal:
reason?.match(/cancelled by user/i) ||
reason?.match(/cancell?ed by user/i) ||
// For Trust:
reason?.match(/user cancell?ed/i) ||
// For Coinbase:
reason?.match(/user denied/i) ||
// For Fireblocks
reason?.match(/user rejected/i)
) {
return true
}
return false
}
/**
* This is hacking out the revert reason from the ethers provider thrown error however it can.
* This object seems to be undocumented by ethers.
* @param error - An error from the ethers provider
*/
export function swapErrorToUserReadableMessage(error: any): string {
if (didUserReject(error)) {
return t`Transaction rejected`
}
let reason = getReason(error)
if (reason?.indexOf('execution reverted: ') === 0) reason = reason.substr('execution reverted: '.length)
switch (reason) {
case 'UniswapV2Router: EXPIRED':
return t`The transaction could not be sent because the deadline has passed. Please check that your transaction deadline is not too low.`
return t`This transaction could not be sent because the deadline has passed. Please check that your transaction deadline is not too low.`
case 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT':
case 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT':
return t`This transaction will not succeed either due to price movement or fee on transfer. Try increasing your slippage tolerance.`
@@ -63,6 +66,7 @@ export function swapErrorToUserReadableMessage(error: any): string {
return t`The output token cannot be transferred. There may be an issue with the output token. Note: fee on transfer and rebase tokens are incompatible with Uniswap V3.`
default:
if (reason?.indexOf('undefined is not an object') !== -1) {
console.error(error, reason)
return t`An error occurred when trying to execute this swap. You may need to increase your slippage tolerance. If that does not work, there may be an incompatibility with the token you are trading. Note: fee on transfer and rebase tokens are incompatible with Uniswap V3.`
}
return t`${reason ? reason : 'Unknown error'}. Try increasing your slippage tolerance.

View File

@@ -5013,10 +5013,10 @@
react "^18.2.0"
react-dom "^18.2.0"
"@uniswap/conedison@^1.4.0", "@uniswap/conedison@^1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.5.1.tgz#91527cc9928ce0187f30a5eb4abb705b8f0cd013"
integrity sha512-VJqUW4l54QVj5a4vAzAlWWd193iCcT8HMugFPB28S2Uqhs2elAg/RDQmiPOf9TOFB635MdBlD0S6xUuqo7FB4A==
"@uniswap/conedison@^1.4.0", "@uniswap/conedison@^1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.5.3.tgz#2a2fc9ca848644f21944a2d087de54032a6acf93"
integrity sha512-b8j2/0FzqLU4Qq+M+QEPGzacnZxNrzAHp7yoAWRvNJiFyLjBvcgfaT9ORS8rw17M8XBLWjh83faj5Kymc+62qw==
"@uniswap/default-token-list@^2.0.0":
version "2.2.0"
@@ -5274,17 +5274,17 @@
"@uniswap/v3-core" "1.0.0"
"@uniswap/v3-periphery" "^1.0.1"
"@uniswap/widgets@^2.47.3":
version "2.47.3"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.47.3.tgz#d4194c86604199ac717be27396e653e4f8c0f5f8"
integrity sha512-OO9CKMmQAuq7hpnM+SU5t5BynlXLG8S6zCCG/L6ifCueJlThzNjOPhqVbNOxpyeCHVhAGhrZb57EPgfmaZf1Hg==
"@uniswap/widgets@^2.48.5":
version "2.48.5"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.48.5.tgz#19c197d6c87dddbfe22bfcb060a794cfacb1fd03"
integrity sha512-zBQepwfDZKniwjByN4AucmN8T9a8OSA7Gd5KU2rzs1i5t1/Z34Khbp4wOXbWOXYYORjfIhtsyIBIZWoaW7SInA==
dependencies:
"@babel/runtime" ">=7.17.0"
"@fontsource/ibm-plex-mono" "^4.5.1"
"@fontsource/inter" "^4.5.1"
"@popperjs/core" "^2.4.4"
"@reduxjs/toolkit" "^1.6.1"
"@uniswap/conedison" "^1.5.1"
"@uniswap/conedison" "^1.5.3"
"@uniswap/permit2-sdk" "^1.2.0"
"@uniswap/redux-multicall" "^1.1.8"
"@uniswap/router-sdk" "^1.3.0"
@@ -5307,7 +5307,7 @@
cids "^1.0.0"
ethers "^5.7.2"
immer "^9.0.6"
jotai "^1.3.7"
jotai "1.4.0"
jsbi "^3.1.4"
make-plural "^7.0.0"
ms.macro "^2.0.0"
@@ -13105,7 +13105,7 @@ jest@26.6.0:
import-local "^3.0.2"
jest-cli "^26.6.0"
jotai@^1.3.7:
jotai@1.4.0, jotai@^1.3.7:
version "1.4.0"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.4.0.tgz#0f350f65a968dd3ee2f9ad3618a3af635cd10220"
integrity sha512-CUB+A3N+WjtimZvtDnMXvVRognzKh86KB3rKnQlbRvpnmGYU+O9aOZMWSgTaxstXc4Y5GYy02LBEjiv4Rs8MAg==