Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd92e4cf6f | ||
|
|
089fba5dee | ||
|
|
503a33314f | ||
|
|
6fee37c4b8 | ||
|
|
a2812fcf79 | ||
|
|
c42aeae96a | ||
|
|
80ab000d34 | ||
|
|
81206f1eef | ||
|
|
568b05fda1 | ||
|
|
3d0ca21036 | ||
|
|
8b743615d1 | ||
|
|
9e4fdabc34 | ||
|
|
decb922d4b | ||
|
|
ac50555647 | ||
|
|
f48356d0fb | ||
|
|
a362f8797a | ||
|
|
783f42abcc | ||
|
|
0923cf4ac9 | ||
|
|
801958d0ae | ||
|
|
8392c29a1e | ||
|
|
8d36edf2b7 | ||
|
|
0f8d3fa506 | ||
|
|
5f64149f39 | ||
|
|
7115729e3e | ||
|
|
6618135e7d |
11
.env
11
.env
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
22
.github/workflows/test.yml
vendored
22
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Pool', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/pool').then(() => {
|
||||
cy.visit('/pools').then(() => {
|
||||
cy.wait('@eth_blockNumber')
|
||||
})
|
||||
})
|
||||
|
||||
107
cypress/e2e/swap-widget.cy.ts
Normal file
107
cypress/e2e/swap-widget.cy.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Wallet Dropdown', () => {
|
||||
before(() => {
|
||||
cy.visit('/pool')
|
||||
cy.visit('/pools')
|
||||
})
|
||||
|
||||
it('should change the theme', () => {
|
||||
|
||||
@@ -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}]`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />,
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -258,7 +258,7 @@ export default function TokenSafety({
|
||||
}
|
||||
|
||||
return displayWarning ? (
|
||||
<Wrapper>
|
||||
<Wrapper data-testid="TokenSafetyWrapper">
|
||||
<Container>
|
||||
<AutoColumn>
|
||||
<LogoContainer>{logos}</LogoContainer>
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useGqlRoutingFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.gqlRouting, BaseVariant.Enabled)
|
||||
}
|
||||
|
||||
export { BaseVariant as GqlRoutingVariant }
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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') ||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
44
src/nft/hooks/useBagTotalEthPrice.ts
Normal file
44
src/nft/hooks/useBagTotalEthPrice.ts
Normal 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])
|
||||
}
|
||||
102
src/nft/hooks/useFetchAssets.ts
Normal file
102
src/nft/hooks/useFetchAssets.ts
Normal 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,
|
||||
])
|
||||
}
|
||||
52
src/nft/hooks/usePurchaseAssets.ts
Normal file
52
src/nft/hooks/usePurchaseAssets.ts
Normal 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]
|
||||
)
|
||||
}
|
||||
@@ -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[]) => {
|
||||
|
||||
38
src/nft/hooks/useSubscribeTransactionState.ts
Normal file
38
src/nft/hooks/useSubscribeTransactionState.ts
Normal 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])
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './ActivityFetcher'
|
||||
export * from './CollectionPreviewFetcher'
|
||||
export * from './logListing'
|
||||
export * from './RouteFetcher'
|
||||
export * from './SearchCollectionsFetcher'
|
||||
export * from './TrendingCollectionsFetcher'
|
||||
|
||||
@@ -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
75
src/nft/utils/bag.ts
Normal 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 }
|
||||
}
|
||||
@@ -74,7 +74,7 @@ const itemInRouteAndSamePool = (
|
||||
)
|
||||
}
|
||||
|
||||
export const combineBuyItemsWithTxRoute = (
|
||||
export const compareAssetsWithTransactionRoute = (
|
||||
items: UpdatedGenieAsset[],
|
||||
txRoute?: RoutingItem[]
|
||||
): { hasPriceAdjustment: boolean; updatedAssets: UpdatedGenieAsset[] } => {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
Don’t 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SerializedPair, SerializedToken } from './types'
|
||||
const currentTimestamp = () => new Date().getTime()
|
||||
|
||||
export interface UserState {
|
||||
taxServiceDismissals: number
|
||||
taxServiceDismissals: number | undefined
|
||||
|
||||
selectedWallet?: ConnectionType
|
||||
|
||||
|
||||
@@ -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
144
src/tracing/trace.test.ts
Normal 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
89
src/tracing/trace.ts
Normal 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)
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
22
yarn.lock
22
yarn.lock
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user