Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0208ccd7d2 | ||
|
|
12df4b3981 | ||
|
|
3eaeb65b07 | ||
|
|
6df2f3677e | ||
|
|
80edf5a0d6 | ||
|
|
96f6929127 | ||
|
|
4ec95d0927 | ||
|
|
fba6cc9e02 | ||
|
|
bc2f68565b | ||
|
|
f232643d8e | ||
|
|
527270e33f | ||
|
|
18cd5ec9d9 | ||
|
|
5ddb565805 | ||
|
|
f0b4b92b88 | ||
|
|
4d82f9fb3a | ||
|
|
654b26dc54 | ||
|
|
c0753ae52f | ||
|
|
16bb9470ae | ||
|
|
6dcfca24cb | ||
|
|
9cac9f8299 | ||
|
|
5def0dd166 | ||
|
|
7229637c4c | ||
|
|
f26b09537d | ||
|
|
8f922b665a | ||
|
|
134b1d708f | ||
|
|
e9bddcb670 | ||
|
|
19e45fd119 | ||
|
|
ae4135fa49 | ||
|
|
89e438bcc5 | ||
|
|
92af2167ee | ||
|
|
db6084d717 | ||
|
|
927d35d59e | ||
|
|
b4e981b2fd | ||
|
|
967a698178 | ||
|
|
7818426b53 | ||
|
|
93e0054f10 | ||
|
|
661d2b6a33 | ||
|
|
c560b94366 | ||
|
|
93a4f00287 | ||
|
|
48833f27e3 | ||
|
|
35a03e2681 | ||
|
|
ac0badfb1d | ||
|
|
149b18f02e | ||
|
|
52a43f3db0 | ||
|
|
0a2a46d506 | ||
|
|
a7c1bd4391 | ||
|
|
13221e6935 | ||
|
|
26fc3caa55 | ||
|
|
6072bb1be0 |
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -30,6 +30,11 @@ jobs:
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn test
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
cypress-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
10
README.md
10
README.md
@@ -1,5 +1,7 @@
|
||||
# Uniswap Labs Interface
|
||||
|
||||
[](https://codecov.io/gh/Uniswap/interface)
|
||||
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml)
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/lint.yml)
|
||||
@@ -40,10 +42,10 @@ 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
|
||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
||||
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
|
||||
- View V2 liquidity: <https://app.uniswap.org/#/pool/v2>
|
||||
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
|
||||
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
beforeEach(() => {
|
||||
@@ -16,7 +17,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should load pudgy penguin collection page', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-collection-asset')).should('exist')
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
@@ -24,13 +25,13 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should be able to navigate to activity', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-activity')).first().click()
|
||||
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
||||
})
|
||||
|
||||
it('should go to the details page', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||
cy.get(getTestSelector('nft-details-link')).first().click()
|
||||
@@ -41,7 +42,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should toggle buy now on details page', () => {
|
||||
cy.visit(`#/nfts/asset/${COLLECTION_ADDRESS}/2043`)
|
||||
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
||||
cy.get(getTestSelector('nft-details-description')).click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||
|
||||
12
package.json
12
package.json
@@ -135,7 +135,7 @@
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "1.2.0",
|
||||
"@uniswap/analytics-events": "^2.1.0",
|
||||
"@uniswap/conedison": "^1.2.1",
|
||||
"@uniswap/conedison": "^1.3.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
@@ -152,7 +152,7 @@
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "^2.26.0",
|
||||
"@uniswap/widgets": "^2.27.0",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
@@ -165,16 +165,16 @@
|
||||
"@visx/responsive": "^2.10.0",
|
||||
"@visx/shape": "^2.11.1",
|
||||
"@walletconnect/ethereum-provider": "^1.8.0",
|
||||
"@web3-react/coinbase-wallet": "8.0.34-beta.0",
|
||||
"@web3-react/coinbase-wallet": "8.0.35-beta.0",
|
||||
"@web3-react/core": "8.0.35-beta.0",
|
||||
"@web3-react/eip1193": "8.0.26-beta.0",
|
||||
"@web3-react/eip1193": "8.0.27-beta.0",
|
||||
"@web3-react/empty": "8.0.20-beta.0",
|
||||
"@web3-react/gnosis-safe": "8.0.7-beta.0",
|
||||
"@web3-react/metamask": "8.0.29-beta.0",
|
||||
"@web3-react/metamask": "8.0.30-beta.0",
|
||||
"@web3-react/network": "8.0.27-beta.0",
|
||||
"@web3-react/types": "8.0.20-beta.0",
|
||||
"@web3-react/url": "8.0.25-beta.0",
|
||||
"@web3-react/walletconnect": "8.0.36-beta.0",
|
||||
"@web3-react/walletconnect": "8.0.37-beta.0",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"cids": "^1.0.0",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ExternalLink } from 'theme'
|
||||
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
|
||||
|
||||
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'
|
||||
import darkUnicornImgSrc from './images/unicornEmbossDark.png'
|
||||
@@ -97,14 +96,10 @@ const ExternalTextLink = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const TextLink = styled(Link)`
|
||||
const TextLink = styled(StyledRouterLink)`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const Copyright = styled.span`
|
||||
@@ -158,15 +153,9 @@ export const AboutFooter = () => {
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
<LinkGroupTitle>Protocol</LinkGroupTitle>
|
||||
<ExternalTextLink href="https://uniswap.org/community" target="_blank" rel="noopener noreferrer">
|
||||
Community
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/governance" target="_blank" rel="noopener noreferrer">
|
||||
Governance
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/developers" target="_blank" rel="noopener noreferrer">
|
||||
Developers
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/community">Community</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/governance">Governance</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/developers">Developers</ExternalTextLink>
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
<LinkGroupTitle>Company</LinkGroupTitle>
|
||||
@@ -175,18 +164,14 @@ export const AboutFooter = () => {
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.CAREERS_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs" target="_blank" rel="noopener noreferrer">
|
||||
Careers
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs">Careers</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.BLOG_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://uniswap.org/blog" target="_blank" rel="noopener noreferrer">
|
||||
Blog
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/blog">Blog</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
@@ -209,9 +194,7 @@ export const AboutFooter = () => {
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.SUPPORT_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://support.uniswap.org/hc/en-us" target="_blank" rel="noopener noreferrer">
|
||||
Help Center
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://support.uniswap.org/hc/en-us">Help Center</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
</LinkGroup>
|
||||
</FooterLinks>
|
||||
|
||||
@@ -97,7 +97,8 @@ export const ButtonPrimary = styled(BaseButton)`
|
||||
export const SmallButtonPrimary = styled(ButtonPrimary)`
|
||||
width: auto;
|
||||
font-size: 16px;
|
||||
padding: 10px 16px;
|
||||
padding: ${({ padding }) => padding ?? '8px 12px'};
|
||||
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ interface DoubleCurrencyLogoProps {
|
||||
currency1?: Currency
|
||||
}
|
||||
|
||||
const HigherLogo = styled(CurrencyLogo)`
|
||||
const HigherLogoWrapper = styled.div`
|
||||
z-index: 1;
|
||||
`
|
||||
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
|
||||
const CoveredLogoWapper = styled.div<{ sizeraw: number }>`
|
||||
position: absolute;
|
||||
left: ${({ sizeraw }) => '-' + (sizeraw / 2).toString() + 'px'} !important;
|
||||
`
|
||||
@@ -33,8 +33,16 @@ export default function DoubleCurrencyLogo({
|
||||
}: DoubleCurrencyLogoProps) {
|
||||
return (
|
||||
<Wrapper sizeraw={size} margin={margin}>
|
||||
{currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
|
||||
{currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
|
||||
{currency0 && (
|
||||
<HigherLogoWrapper>
|
||||
<CurrencyLogo hideL2Icon currency={currency0} size={size.toString() + 'px'} />
|
||||
</HigherLogoWrapper>
|
||||
)}
|
||||
{currency1 && (
|
||||
<CoveredLogoWapper sizeraw={size}>
|
||||
<CurrencyLogo hideL2Icon currency={currency1} size={size.toString() + 'px'} />
|
||||
</CoveredLogoWapper>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
|
||||
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
||||
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||
@@ -229,6 +231,18 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.payWithAnyToken}
|
||||
label="Pay With Any Token"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={SwapWidgetVariant}
|
||||
value={useSwapWidgetFlag()}
|
||||
featureFlag={FeatureFlag.swapWidget}
|
||||
label="Swap Widget"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={GqlRoutingVariant}
|
||||
value={useGqlRoutingFlag()}
|
||||
featureFlag={FeatureFlag.gqlRouting}
|
||||
label="GraphQL NFT Routing"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
||||
import React from 'react'
|
||||
@@ -29,10 +30,28 @@ export type AssetLogoBaseProps = {
|
||||
backupImg?: string | null
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
hideL2Icon?: boolean
|
||||
}
|
||||
type AssetLogoProps = AssetLogoBaseProps & { isNative?: boolean; address?: string | null; chainId?: number }
|
||||
|
||||
// TODO(cartcrom): add prop to optionally render an L2Icon w/ the logo
|
||||
const LogoContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const L2NetworkLogo = styled.div<{ networkUrl?: string; parentSize: string }>`
|
||||
--size: ${({ parentSize }) => `calc(${parentSize} / 2)`};
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
background: url(${({ networkUrl }) => networkUrl});
|
||||
background-repeat: no-repeat;
|
||||
background-size: ${({ parentSize }) => `calc(${parentSize} / 2) calc(${parentSize} / 2)`};
|
||||
display: ${({ networkUrl }) => !networkUrl && 'none'};
|
||||
`
|
||||
|
||||
/**
|
||||
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
|
||||
*/
|
||||
@@ -44,25 +63,27 @@ export default function AssetLogo({
|
||||
backupImg,
|
||||
size = '24px',
|
||||
style,
|
||||
...rest
|
||||
hideL2Icon = false,
|
||||
}: AssetLogoProps) {
|
||||
const imageProps = {
|
||||
alt: `${symbol ?? 'token'} logo`,
|
||||
size,
|
||||
style,
|
||||
...rest,
|
||||
}
|
||||
|
||||
const [src, nextSrc] = useTokenLogoSource(address, chainId, isNative, backupImg)
|
||||
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
|
||||
|
||||
if (src) {
|
||||
return <LogoImage {...imageProps} src={src} onError={nextSrc} />
|
||||
} else {
|
||||
return (
|
||||
<MissingImageLogo size={size}>
|
||||
{/* use only first 3 characters of Symbol for design reasons */}
|
||||
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
|
||||
</MissingImageLogo>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<LogoContainer style={style}>
|
||||
{src ? (
|
||||
<LogoImage {...imageProps} src={src} onError={nextSrc} />
|
||||
) : (
|
||||
<MissingImageLogo size={size}>
|
||||
{/* use only first 3 characters of Symbol for design reasons */}
|
||||
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
|
||||
</MissingImageLogo>
|
||||
)}
|
||||
{!hideL2Icon && <L2NetworkLogo networkUrl={L2Icon} parentSize={size} />}
|
||||
</LogoContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default function CurrencyLogo(
|
||||
address={props.currency?.wrapped.address}
|
||||
symbol={props.symbol ?? props.currency?.symbol}
|
||||
backupImg={(props.currency as TokenInfo)?.logoURI}
|
||||
hideL2Icon={props.hideL2Icon ?? true}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -166,6 +166,9 @@ export const MenuDropdown = () => {
|
||||
<SecondaryLinkedText href="https://docs.uniswap.org/">
|
||||
<Trans>Documentation</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText href="https://uniswap.canny.io/feature-requests">
|
||||
<Trans>Feedback</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText
|
||||
onClick={() => {
|
||||
toggleOpen()
|
||||
|
||||
@@ -4,7 +4,6 @@ import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import clsx from 'clsx'
|
||||
import AssetLogo from 'components/Logo/AssetLogo'
|
||||
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
@@ -25,9 +24,6 @@ import styled from 'styled-components/macro'
|
||||
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
|
||||
import * as styles from './SearchBar.css'
|
||||
|
||||
const StyledLogoContainer = styled(LogoContainer)`
|
||||
margin-right: 8px;
|
||||
`
|
||||
const PriceChangeContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -160,7 +156,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addToSearchHistory, toggleOpen, token, eventProperties])
|
||||
|
||||
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
|
||||
const [bridgedAddress, bridgedChain] = useBridgedAddress(token)
|
||||
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
|
||||
// Close the modal on escape
|
||||
useEffect(() => {
|
||||
@@ -190,17 +186,15 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
|
||||
>
|
||||
<Row style={{ width: '65%' }}>
|
||||
<StyledLogoContainer>
|
||||
<AssetLogo
|
||||
isNative={token.address === NATIVE_CHAIN_ID}
|
||||
address={token.address}
|
||||
chainId={token.chainId}
|
||||
symbol={token.symbol}
|
||||
size="36px"
|
||||
backupImg={token.logoURI}
|
||||
/>
|
||||
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
|
||||
</StyledLogoContainer>
|
||||
<AssetLogo
|
||||
isNative={token.address === NATIVE_CHAIN_ID}
|
||||
address={token.address}
|
||||
chainId={token.chainId}
|
||||
symbol={token.symbol}
|
||||
size="36px"
|
||||
backupImg={token.logoURI}
|
||||
style={{ margin: '8px 8px 8px 0' }}
|
||||
/>
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{token.name}</Box>
|
||||
|
||||
@@ -57,8 +57,7 @@ export const PageTabs = () => {
|
||||
pathname.startsWith('/pool') ||
|
||||
pathname.startsWith('/add') ||
|
||||
pathname.startsWith('/remove') ||
|
||||
pathname.startsWith('/increase') ||
|
||||
pathname.startsWith('/find')
|
||||
pathname.startsWith('/increase')
|
||||
|
||||
const isNftPage = useIsNftPage()
|
||||
|
||||
|
||||
81
src/components/Polling/ChainConnectivityWarning.tsx
Normal file
81
src/components/Polling/ChainConnectivityWarning.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getChainInfoOrDefault, L2ChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
|
||||
|
||||
const BodyRow = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
`
|
||||
const CautionTriangle = styled(AlertTriangle)`
|
||||
color: ${({ theme }) => theme.accentWarning};
|
||||
`
|
||||
const Link = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.black};
|
||||
text-decoration: underline;
|
||||
`
|
||||
const TitleRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
const TitleText = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin: 0px 12px;
|
||||
`
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
bottom: 60px;
|
||||
display: none;
|
||||
max-width: 348px;
|
||||
padding: 16px 20px;
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium}px) {
|
||||
display: block;
|
||||
}
|
||||
`
|
||||
|
||||
export function ChainConnectivityWarning() {
|
||||
const { chainId } = useWeb3React()
|
||||
const info = getChainInfoOrDefault(chainId)
|
||||
const label = info?.label
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TitleRow>
|
||||
<CautionTriangle />
|
||||
<TitleText>
|
||||
<Trans>Network Warning</Trans>
|
||||
</TitleText>
|
||||
</TitleRow>
|
||||
<BodyRow>
|
||||
{chainId === SupportedChainId.MAINNET ? (
|
||||
<Trans>You may have lost your network connection.</Trans>
|
||||
) : (
|
||||
<Trans>{label} might be down right now, or you may have lost your network connection.</Trans>
|
||||
)}{' '}
|
||||
{(info as L2ChainInfo).statusPage !== undefined && (
|
||||
<span>
|
||||
<Trans>Check network status</Trans>{' '}
|
||||
<Link href={(info as L2ChainInfo).statusPage || ''}>
|
||||
<Trans>here.</Trans>
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
</BodyRow>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
164
src/components/Polling/index.tsx
Normal file
164
src/components/Polling/index.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import { useIsLandingPage } from 'hooks/useIsLandingPage'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import useMachineTimeMs from 'hooks/useMachineTime'
|
||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import { ChainConnectivityWarning } from './ChainConnectivityWarning'
|
||||
|
||||
const StyledPolling = styled.div`
|
||||
align-items: center;
|
||||
bottom: 0;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
display: none;
|
||||
padding: 1rem;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
transition: 250ms ease color;
|
||||
|
||||
a {
|
||||
color: unset;
|
||||
}
|
||||
a:hover {
|
||||
color: unset;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
const StyledPollingBlockNumber = styled(ThemedText.DeprecatedSmall)<{
|
||||
breathe: boolean
|
||||
hovering: boolean
|
||||
warning: boolean
|
||||
}>`
|
||||
color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
|
||||
transition: opacity 0.25s ease;
|
||||
opacity: ${({ breathe, hovering }) => (hovering ? 0.7 : breathe ? 1 : 0.5)};
|
||||
:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
a {
|
||||
color: unset;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
color: unset;
|
||||
}
|
||||
`
|
||||
const StyledPollingDot = styled.div<{ warning: boolean }>`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
min-height: 8px;
|
||||
min-width: 8px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
background-color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
|
||||
transition: 250ms ease background-color;
|
||||
`
|
||||
|
||||
const rotate360 = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`
|
||||
|
||||
const Spinner = styled.div<{ warning: boolean }>`
|
||||
animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite;
|
||||
transform: translateZ(0);
|
||||
|
||||
border-top: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-left: 2px solid ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
|
||||
background: transparent;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
transition: 250ms ease border-color;
|
||||
|
||||
left: -3px;
|
||||
top: -3px;
|
||||
`
|
||||
|
||||
const DEFAULT_MS_BEFORE_WARNING = ms`10m`
|
||||
const NETWORK_HEALTH_CHECK_MS = ms`10s`
|
||||
|
||||
export default function Polling() {
|
||||
const { chainId } = useWeb3React()
|
||||
const blockNumber = useBlockNumber()
|
||||
const [isMounting, setIsMounting] = useState(false)
|
||||
const [isHover, setIsHover] = useState(false)
|
||||
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
|
||||
const blockTime = useCurrentBlockTimestamp()
|
||||
const isNftPage = useIsNftPage()
|
||||
const isLandingPage = useIsLandingPage()
|
||||
|
||||
const waitMsBeforeWarning =
|
||||
(chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
|
||||
|
||||
const warning = Boolean(!!blockTime && machineTime - blockTime.mul(1000).toNumber() > waitMsBeforeWarning)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!blockNumber) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsMounting(true)
|
||||
const mountingTimer = setTimeout(() => setIsMounting(false), 1000)
|
||||
|
||||
// this will clear Timeout when component unmount like in willComponentUnmount
|
||||
return () => {
|
||||
clearTimeout(mountingTimer)
|
||||
}
|
||||
},
|
||||
[blockNumber] //useEffect will run only one time
|
||||
//if you pass a value to array, like this [data] than clearTimeout will run every time this value changes (useEffect re-run)
|
||||
)
|
||||
|
||||
//TODO - chainlink gas oracle is really slow. Can we get a better data source?
|
||||
|
||||
const blockExternalLinkHref = useMemo(() => {
|
||||
if (!chainId || !blockNumber) return ''
|
||||
return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK)
|
||||
}, [blockNumber, chainId])
|
||||
|
||||
if (isNftPage || isLandingPage) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<RowFixed>
|
||||
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
|
||||
<StyledPollingBlockNumber breathe={isMounting} hovering={isHover} warning={warning}>
|
||||
<ExternalLink href={blockExternalLinkHref}>
|
||||
<MouseoverTooltip
|
||||
text={<Trans>The most recent block number on this network. Prices update on every block.</Trans>}
|
||||
>
|
||||
{blockNumber} 
|
||||
</MouseoverTooltip>
|
||||
</ExternalLink>
|
||||
</StyledPollingBlockNumber>
|
||||
<StyledPollingDot warning={warning}>{isMounting && <Spinner warning={warning} />}</StyledPollingDot>{' '}
|
||||
</StyledPolling>
|
||||
{warning && <ChainConnectivityWarning />}
|
||||
</RowFixed>
|
||||
)
|
||||
}
|
||||
@@ -105,235 +105,240 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
}
|
||||
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
style="padding-right: 8px; padding-top: 8px;"
|
||||
>
|
||||
<div
|
||||
style="height: 168px; width: 100%;"
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
style="height: 168px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
class="c6 css-vurnku"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
DAI
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="USD//C"
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
USD//C
|
||||
DAI
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
class="c6 css-vurnku"
|
||||
title="USD//C"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
USD//C
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Wrapped BTC"
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
Wrapped BTC
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
class="c6 css-vurnku"
|
||||
title="Wrapped BTC"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
WBTC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
class="c4"
|
||||
>
|
||||
WBTC
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -383,31 +388,36 @@ exports[`renders loading rows when isLoading is true 1`] = `
|
||||
}
|
||||
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
style="padding-right: 8px; padding-top: 8px;"
|
||||
>
|
||||
<div
|
||||
style="height: 560px; width: 100%;"
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
style="height: 560px; width: 100%;"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
20
src/components/SearchModal/CurrencyList/index.css.ts
Normal file
20
src/components/SearchModal/CurrencyList/index.css.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const scrollbarStyle = style([
|
||||
{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${themeVars.colors.backgroundOutline} transparent`,
|
||||
height: '100%',
|
||||
selectors: {
|
||||
'&::-webkit-scrollbar': {
|
||||
background: 'transparent',
|
||||
width: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: `${themeVars.colors.backgroundOutline}`,
|
||||
borderRadius: '8px',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -20,6 +20,7 @@ import CurrencyLogo from '../../Logo/CurrencyLogo'
|
||||
import Row, { RowFixed } from '../../Row'
|
||||
import { MouseoverTooltip } from '../../Tooltip'
|
||||
import { LoadingRows, MenuItem } from '../styleds'
|
||||
import * as styles from './index.css'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency.isToken ? currency.address : 'ETHER'
|
||||
@@ -288,21 +289,34 @@ export default function CurrencyList({
|
||||
return currencyKey(currency)
|
||||
}, [])
|
||||
|
||||
return isLoading ? (
|
||||
<FixedSizeList height={height} ref={fixedListRef as any} width="100%" itemData={[]} itemCount={10} itemSize={56}>
|
||||
{LoadingRow}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
itemData={itemData}
|
||||
itemCount={itemData.length}
|
||||
itemSize={56}
|
||||
itemKey={itemKey}
|
||||
>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
return (
|
||||
<div style={{ paddingRight: '8px', paddingTop: '8px' }}>
|
||||
{isLoading ? (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
itemData={[]}
|
||||
itemCount={10}
|
||||
itemSize={56}
|
||||
>
|
||||
{LoadingRow}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
itemData={itemData}
|
||||
itemCount={itemData.length}
|
||||
itemSize={56}
|
||||
itemKey={itemKey}
|
||||
>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { PaddedColumn, SearchInput, Separator } from './styleds'
|
||||
const ContentWrapper = styled(Column)`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1 1;
|
||||
position: relative;
|
||||
`
|
||||
@@ -45,6 +46,7 @@ interface CurrencySearchProps {
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
onlyShowCurrenciesWithBalance?: boolean
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
@@ -56,6 +58,7 @@ export function CurrencySearch({
|
||||
disableNonToken,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
}: CurrencySearchProps) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
@@ -92,6 +95,10 @@ export function CurrencySearch({
|
||||
!balancesAreLoading
|
||||
? filteredTokens
|
||||
.filter((token) => {
|
||||
if (onlyShowCurrenciesWithBalance) {
|
||||
return balances[token.address]?.greaterThan(0)
|
||||
}
|
||||
|
||||
// If there is no query, filter out unselected user-added tokens with no balance.
|
||||
if (!debouncedQuery && token instanceof UserAddedToken) {
|
||||
if (selectedCurrency?.equals(token) || otherSelectedCurrency?.equals(token)) return true
|
||||
@@ -101,7 +108,15 @@ export function CurrencySearch({
|
||||
})
|
||||
.sort(tokenComparator.bind(null, balances))
|
||||
: [],
|
||||
[balances, balancesAreLoading, debouncedQuery, filteredTokens, otherSelectedCurrency, selectedCurrency]
|
||||
[
|
||||
balances,
|
||||
balancesAreLoading,
|
||||
debouncedQuery,
|
||||
filteredTokens,
|
||||
otherSelectedCurrency,
|
||||
selectedCurrency,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
]
|
||||
)
|
||||
const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed)
|
||||
|
||||
@@ -114,11 +129,23 @@ export function CurrencySearch({
|
||||
const s = debouncedQuery.toLowerCase().trim()
|
||||
|
||||
const tokens = filteredSortedTokens.filter((t) => !(t.equals(wrapped) || (disableNonToken && t.isNative)))
|
||||
const natives = (disableNonToken || native.equals(wrapped) ? [wrapped] : [native, wrapped]).filter(
|
||||
(n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1
|
||||
)
|
||||
const shouldShowWrapped =
|
||||
!onlyShowCurrenciesWithBalance || (!balancesAreLoading && balances[wrapped.address]?.greaterThan(0))
|
||||
const natives = (
|
||||
disableNonToken || native.equals(wrapped) ? [wrapped] : shouldShowWrapped ? [native, wrapped] : [native]
|
||||
).filter((n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1)
|
||||
|
||||
return [...natives, ...tokens]
|
||||
}, [debouncedQuery, filteredSortedTokens, wrapped, disableNonToken, native])
|
||||
}, [
|
||||
debouncedQuery,
|
||||
filteredSortedTokens,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
balancesAreLoading,
|
||||
balances,
|
||||
wrapped,
|
||||
disableNonToken,
|
||||
native,
|
||||
])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency, hasWarning?: boolean) => {
|
||||
@@ -168,7 +195,9 @@ export function CurrencySearch({
|
||||
|
||||
// if no results on main list, show option to expand into inactive
|
||||
const filteredInactiveTokens = useSearchInactiveTokenLists(
|
||||
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
|
||||
!onlyShowCurrenciesWithBalance && (filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch))
|
||||
? debouncedQuery
|
||||
: undefined
|
||||
)
|
||||
|
||||
// Timeout token loader after 3 seconds to avoid hanging in a loading state.
|
||||
|
||||
@@ -17,6 +17,7 @@ interface CurrencySearchModalProps {
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
onlyShowCurrenciesWithBalance?: boolean
|
||||
}
|
||||
|
||||
enum CurrencyModalView {
|
||||
@@ -34,6 +35,7 @@ export default memo(function CurrencySearchModal({
|
||||
showCommonBases = false,
|
||||
showCurrencyAmount = true,
|
||||
disableNonToken = false,
|
||||
onlyShowCurrenciesWithBalance = false,
|
||||
}: CurrencySearchModalProps) {
|
||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.search)
|
||||
const lastOpen = useLast(isOpen)
|
||||
@@ -84,6 +86,7 @@ export default memo(function CurrencySearchModal({
|
||||
showCommonBases={showCommonBases}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
disableNonToken={disableNonToken}
|
||||
onlyShowCurrenciesWithBalance={onlyShowCurrenciesWithBalance}
|
||||
/>
|
||||
)
|
||||
break
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function BalanceSummary({ token }: { token: Currency }) {
|
||||
<Trans>Your balance on {label}</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<BalanceRow>
|
||||
<CurrencyLogo currency={token} size="2rem" />
|
||||
<CurrencyLogo currency={token} size="2rem" hideL2Icon={false} />
|
||||
<BalanceContainer>
|
||||
<BalanceAmountsContainer>
|
||||
<BalanceItem>
|
||||
|
||||
@@ -13,7 +13,7 @@ import TimePeriodSelector from './TimeSelector'
|
||||
function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined {
|
||||
// Appends the current price to the end of the priceHistory array
|
||||
const priceHistory = useMemo(() => {
|
||||
const market = tokenPriceData.tokens?.[0]?.market
|
||||
const market = tokenPriceData.token?.market
|
||||
const priceHistory = market?.priceHistory?.filter(isPricePoint)
|
||||
const currentPrice = market?.price?.value
|
||||
if (Array.isArray(priceHistory) && currentPrice !== undefined) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { WidgetSkeleton } from 'components/Widget'
|
||||
import { WIDGET_WIDTH } from 'components/Widget'
|
||||
import { DEFAULT_WIDGET_WIDTH } from 'components/Widget'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { textFadeIn } from 'theme/styles'
|
||||
|
||||
import { LoadingBubble } from '../loading'
|
||||
import { LogoContainer } from '../TokenTable/TokenRow'
|
||||
import { AboutContainer, AboutHeader } from './About'
|
||||
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
|
||||
import { TokenPrice } from './PriceChart'
|
||||
@@ -44,7 +43,7 @@ export const RightPanel = styled.div`
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: ${WIDGET_WIDTH}px;
|
||||
width: ${DEFAULT_WIDGET_WIDTH}px;
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
|
||||
display: flex;
|
||||
@@ -227,9 +226,7 @@ export default function TokenDetailsSkeleton() {
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<LogoContainer>
|
||||
<TokenLogoBubble />
|
||||
</LogoContainer>
|
||||
<TokenLogoBubble />
|
||||
<TitleBubble />
|
||||
</TokenNameCell>
|
||||
</TokenInfoContainer>
|
||||
|
||||
@@ -20,7 +20,6 @@ import TokenDetailsSkeleton, {
|
||||
TokenNameCell,
|
||||
} from 'components/Tokens/TokenDetails/Skeleton'
|
||||
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
||||
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import Widget from 'components/Widget'
|
||||
@@ -111,7 +110,7 @@ export default function TokenDetails({
|
||||
|
||||
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
|
||||
const tokenQueryData = tokenQuery.tokens?.[0]
|
||||
const tokenQueryData = tokenQuery.token
|
||||
const crossChainMap = useMemo(
|
||||
() =>
|
||||
tokenQueryData?.project?.tokens.reduce((map, current) => {
|
||||
@@ -153,6 +152,8 @@ export default function TokenDetails({
|
||||
|
||||
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
|
||||
|
||||
const [openTokenSafetyModal, setOpenTokenSafetyModal] = useState(false)
|
||||
|
||||
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
|
||||
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(address, pageChainId) && tokenWarning !== null
|
||||
const onReviewSwapClick = useCallback(
|
||||
@@ -168,8 +169,6 @@ export default function TokenDetails({
|
||||
[continueSwap, setContinueSwap]
|
||||
)
|
||||
|
||||
const L2Icon = getChainInfo(pageChainId)?.circleLogoUrl
|
||||
|
||||
// address will never be undefined if token is defined; address is checked here to appease typechecker
|
||||
if (token === undefined || !address) {
|
||||
return <InvalidTokenDetails chainName={address && getChainInfo(pageChainId)?.label} />
|
||||
@@ -188,10 +187,8 @@ export default function TokenDetails({
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer data-testid="token-info-container">
|
||||
<TokenNameCell>
|
||||
<LogoContainer>
|
||||
<CurrencyLogo currency={token} size="32px" />
|
||||
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
|
||||
</LogoContainer>
|
||||
<CurrencyLogo currency={token} size="32px" hideL2Icon={false} />
|
||||
|
||||
{token.name ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
</TokenNameCell>
|
||||
@@ -220,22 +217,28 @@ export default function TokenDetails({
|
||||
<TokenDetailsSkeleton />
|
||||
)}
|
||||
|
||||
<RightPanel>
|
||||
<Widget
|
||||
token={token ?? undefined}
|
||||
onTokenChange={navigateToWidgetSelectedToken}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
/>
|
||||
<RightPanel onClick={() => isBlockedToken && setOpenTokenSafetyModal(true)}>
|
||||
<div style={{ pointerEvents: isBlockedToken ? 'none' : 'auto' }}>
|
||||
<Widget
|
||||
defaultTokens={{
|
||||
default: token ?? undefined,
|
||||
}}
|
||||
onDefaultTokenChange={navigateToWidgetSelectedToken}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
/>
|
||||
</div>
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
|
||||
{token && <BalanceSummary token={token} />}
|
||||
</RightPanel>
|
||||
{token && <MobileBalanceSummaryFooter token={token} />}
|
||||
|
||||
<TokenSafetyModal
|
||||
isOpen={isBlockedToken || !!continueSwap}
|
||||
isOpen={openTokenSafetyModal || !!continueSwap}
|
||||
tokenAddress={address}
|
||||
onContinue={() => onResolveSwap(true)}
|
||||
onBlocked={() => navigate(-1)}
|
||||
onBlocked={() => {
|
||||
setOpenTokenSafetyModal(false)
|
||||
}}
|
||||
onCancel={() => onResolveSwap(false)}
|
||||
showCancel={true}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ParentSize } from '@visx/responsive'
|
||||
import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
@@ -279,23 +278,6 @@ export const SparkLineLoadingBubble = styled(LongLoadingBubble)`
|
||||
height: 4px;
|
||||
`
|
||||
|
||||
export const L2NetworkLogo = styled.div<{ networkUrl?: string; size?: string }>`
|
||||
height: ${({ size }) => size ?? '12px'};
|
||||
width: ${({ size }) => size ?? '12px'};
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
background: url(${({ networkUrl }) => networkUrl});
|
||||
background-repeat: no-repeat;
|
||||
background-size: ${({ size }) => (size ? `${size} ${size}` : '12px 12px')};
|
||||
display: ${({ networkUrl }) => !networkUrl && 'none'};
|
||||
`
|
||||
export const LogoContainer = styled.div`
|
||||
position: relative;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const InfoIconContainer = styled.div`
|
||||
margin-left: 2px;
|
||||
display: flex;
|
||||
@@ -442,18 +424,17 @@ interface LoadedRowProps {
|
||||
tokenListLength: number
|
||||
token: NonNullable<TopToken>
|
||||
sparklineMap: SparklineMap
|
||||
volumeRank: number
|
||||
sortRank: number
|
||||
}
|
||||
|
||||
/* Loaded State: row component with token information */
|
||||
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const { tokenListIndex, tokenListLength, token, volumeRank } = props
|
||||
const { tokenListIndex, tokenListLength, token, sortRank } = props
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
|
||||
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
|
||||
const filterNetwork = lowercaseChainName.toUpperCase()
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[filterNetwork]
|
||||
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
|
||||
const timePeriod = useAtomValue(filterTimeAtom)
|
||||
const delta = token.market?.pricePercentChange?.value
|
||||
const arrow = getDeltaArrow(delta)
|
||||
@@ -465,7 +446,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
token_address: token.address,
|
||||
token_symbol: token.symbol,
|
||||
token_list_index: tokenListIndex,
|
||||
token_list_rank: volumeRank,
|
||||
token_list_rank: sortRank,
|
||||
token_list_length: tokenListLength,
|
||||
time_frame: timePeriod,
|
||||
search_token_address_input: filterString,
|
||||
@@ -482,13 +463,10 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
>
|
||||
<TokenRow
|
||||
header={false}
|
||||
listNumber={volumeRank}
|
||||
listNumber={sortRank}
|
||||
tokenInfo={
|
||||
<ClickableName>
|
||||
<LogoContainer>
|
||||
<QueryTokenLogo token={token} />
|
||||
<L2NetworkLogo networkUrl={L2Icon} />
|
||||
</LogoContainer>
|
||||
<QueryTokenLogo token={token} />
|
||||
<TokenInfoCell>
|
||||
<TokenName data-cy="token-name">{token.name}</TokenName>
|
||||
<TokenSymbol>{token.symbol}</TokenSymbol>
|
||||
|
||||
@@ -76,12 +76,11 @@ function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
|
||||
}
|
||||
|
||||
export default function TokenTable() {
|
||||
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
|
||||
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
|
||||
const { tokens, tokenVolumeRank, loadingTokens, sparklines } = useTopTokens(chainName)
|
||||
const { tokens, tokenSortRank, loadingTokens, sparklines } = useTopTokens(chainName)
|
||||
|
||||
/* loading and error state */
|
||||
if (loadingTokens) {
|
||||
if (loadingTokens && !tokens) {
|
||||
return <LoadingTokenTable rowCount={PAGE_SIZE} />
|
||||
} else if (!tokens) {
|
||||
return (
|
||||
@@ -110,7 +109,7 @@ export default function TokenTable() {
|
||||
tokenListLength={tokens.length}
|
||||
token={token}
|
||||
sparklineMap={sparklines}
|
||||
volumeRank={tokenVolumeRank[token.address]}
|
||||
sortRank={tokenSortRank[token.address]}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -31,12 +31,12 @@ import { useIsDarkMode } from 'state/user/hooks'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { useSyncWidgetInputs } from './inputs'
|
||||
import { DefaultTokens, useSyncWidgetInputs } from './inputs'
|
||||
import { useSyncWidgetSettings } from './settings'
|
||||
import { DARK_THEME, LIGHT_THEME } from './theme'
|
||||
import { useSyncWidgetTransactions } from './transactions'
|
||||
|
||||
export const WIDGET_WIDTH = 360
|
||||
export const DEFAULT_WIDGET_WIDTH = 360
|
||||
|
||||
const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
|
||||
|
||||
@@ -45,16 +45,25 @@ function useWidgetTheme() {
|
||||
}
|
||||
|
||||
interface WidgetProps {
|
||||
token?: Currency
|
||||
onTokenChange?: (token: Currency) => void
|
||||
defaultTokens: DefaultTokens
|
||||
width?: number | string
|
||||
onDefaultTokenChange?: (token: Currency) => void
|
||||
onReviewSwapClick?: OnReviewSwapClick
|
||||
}
|
||||
|
||||
export default function Widget({ token, onTokenChange, onReviewSwapClick }: WidgetProps) {
|
||||
export default function Widget({
|
||||
defaultTokens,
|
||||
width = DEFAULT_WIDGET_WIDTH,
|
||||
onDefaultTokenChange,
|
||||
onReviewSwapClick,
|
||||
}: WidgetProps) {
|
||||
const { connector, provider } = useWeb3React()
|
||||
const locale = useActiveLocale()
|
||||
const theme = useWidgetTheme()
|
||||
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange })
|
||||
const { inputs, tokenSelector } = useSyncWidgetInputs({
|
||||
defaultTokens,
|
||||
onDefaultTokenChange,
|
||||
})
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
@@ -159,7 +168,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
routerUrl={WIDGET_ROUTER_URL}
|
||||
locale={locale}
|
||||
theme={theme}
|
||||
width={WIDGET_WIDTH}
|
||||
width={width}
|
||||
// defaultChainId is excluded - it is always inferred from the passed provider
|
||||
onConnectWalletClick={onConnectWalletClick}
|
||||
provider={provider}
|
||||
@@ -180,7 +189,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
)
|
||||
}
|
||||
|
||||
export function WidgetSkeleton() {
|
||||
export function WidgetSkeleton({ width = DEFAULT_WIDGET_WIDTH }: { width?: number | string }) {
|
||||
const theme = useWidgetTheme()
|
||||
return <SwapWidgetSkeleton theme={theme} width={WIDGET_WIDTH} />
|
||||
return <SwapWidgetSkeleton theme={theme} width={width} />
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ const EMPTY_AMOUNT = ''
|
||||
|
||||
type SwapValue = Required<SwapController>['value']
|
||||
type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT> & { default?: Currency }
|
||||
export type DefaultTokens = Partial<SwapTokens>
|
||||
|
||||
function includesDefaultToken(tokens: SwapTokens) {
|
||||
if (!tokens.default) return true
|
||||
return tokens[Field.INPUT]?.equals(tokens.default) || tokens[Field.OUTPUT]?.equals(tokens.default)
|
||||
function missingDefaultToken(tokens: SwapTokens) {
|
||||
if (!tokens.default) return false
|
||||
return !tokens[Field.INPUT]?.equals(tokens.default) && !tokens[Field.OUTPUT]?.equals(tokens.default)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,27 +21,31 @@ function includesDefaultToken(tokens: SwapTokens) {
|
||||
* Enforces that token is a part of the returned value.
|
||||
*/
|
||||
export function useSyncWidgetInputs({
|
||||
token,
|
||||
onTokenChange,
|
||||
defaultTokens,
|
||||
onDefaultTokenChange,
|
||||
}: {
|
||||
token?: Currency
|
||||
onTokenChange?: (token: Currency) => void
|
||||
defaultTokens: DefaultTokens
|
||||
onDefaultTokenChange?: (token: Currency) => void
|
||||
}) {
|
||||
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
|
||||
|
||||
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
|
||||
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
|
||||
const [tokens, setTokens] = useState<SwapTokens>({ [Field.OUTPUT]: token, default: token })
|
||||
const [tokens, setTokens] = useState<SwapTokens>(defaultTokens)
|
||||
|
||||
useEffect(() => {
|
||||
setTokens((tokens) => {
|
||||
const update = { ...tokens, default: token }
|
||||
if (!includesDefaultToken(update)) {
|
||||
return { [Field.OUTPUT]: update.default, default: update.default }
|
||||
}
|
||||
return update
|
||||
})
|
||||
}, [token])
|
||||
if (!tokens[Field.INPUT] && !tokens[Field.OUTPUT]) {
|
||||
setTokens((tokens) => {
|
||||
const update = {
|
||||
...tokens,
|
||||
[Field.INPUT]: defaultTokens[Field.INPUT] ?? tokens[Field.INPUT],
|
||||
[Field.OUTPUT]: defaultTokens[Field.OUTPUT] ?? tokens[Field.OUTPUT] ?? defaultTokens.default,
|
||||
default: defaultTokens.default,
|
||||
}
|
||||
return update
|
||||
})
|
||||
}
|
||||
}, [defaultTokens, tokens])
|
||||
|
||||
const onAmountChange = useCallback(
|
||||
(field: Field, amount: string, origin?: 'max') => {
|
||||
@@ -96,13 +101,14 @@ export function useSyncWidgetInputs({
|
||||
return type
|
||||
})
|
||||
|
||||
if (!includesDefaultToken(update)) {
|
||||
onTokenChange?.(update[Field.OUTPUT] || selectingToken)
|
||||
if (missingDefaultToken(update)) {
|
||||
onDefaultTokenChange?.(update[Field.OUTPUT] ?? selectingToken)
|
||||
}
|
||||
setTokens(update)
|
||||
},
|
||||
[onTokenChange, selectingField, tokens]
|
||||
[onDefaultTokenChange, selectingField, tokens]
|
||||
)
|
||||
|
||||
const tokenSelector = (
|
||||
<CurrencySearchModal
|
||||
isOpen={selectingField !== undefined}
|
||||
@@ -110,6 +116,7 @@ export function useSyncWidgetInputs({
|
||||
selectedCurrency={selectingField && tokens[selectingField]}
|
||||
otherSelectedCurrency={selectingField && tokens[invertField(selectingField)]}
|
||||
onCurrencySelect={onTokenSelect}
|
||||
showCommonBases
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -117,11 +124,11 @@ export function useSyncWidgetInputs({
|
||||
() => ({
|
||||
type,
|
||||
amount,
|
||||
// If the default has not yet been handled, preemptively disable the widget by passing no tokens. Effectively,
|
||||
// If the initial state has not yet been set, preemptively disable the widget by passing no tokens. Effectively,
|
||||
// this resets the widget - avoiding rendering stale state - because with no tokens the skeleton will be rendered.
|
||||
...(token && tokens.default?.equals(token) ? tokens : undefined),
|
||||
...(tokens[Field.INPUT] || tokens[Field.OUTPUT] ? tokens : undefined),
|
||||
}),
|
||||
[amount, token, tokens, type]
|
||||
[amount, tokens, type]
|
||||
)
|
||||
const valueHandlers: SwapEventHandlers = useMemo(
|
||||
() => ({ onAmountChange, onSwitchTokens, onTokenSelectorClick }),
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Theme } from '@uniswap/widgets'
|
||||
import { darkTheme, lightTheme } from 'theme/colors'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
const zIndex = {
|
||||
modal: Z_INDEX.modal,
|
||||
}
|
||||
|
||||
const fonts = {
|
||||
fontFamily: 'Inter custom',
|
||||
@@ -31,6 +36,7 @@ export const LIGHT_THEME: Theme = {
|
||||
error: lightTheme.accentCritical,
|
||||
|
||||
...fonts,
|
||||
zIndex,
|
||||
}
|
||||
|
||||
export const DARK_THEME: Theme = {
|
||||
@@ -58,4 +64,5 @@ export const DARK_THEME: Theme = {
|
||||
error: darkTheme.accentCritical,
|
||||
|
||||
...fonts,
|
||||
zIndex,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceEventName, InterfaceSectionName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import {
|
||||
OnTxSuccess,
|
||||
TradeType,
|
||||
Transaction,
|
||||
TransactionEventHandlers,
|
||||
TransactionInfo,
|
||||
TransactionType,
|
||||
TransactionType as WidgetTransactionType,
|
||||
} from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { WrapType } from 'hooks/useWrapCallback'
|
||||
import { formatSwapSignedAnalyticsEventProperties, formatToDecimal, getTokenAddress } from 'lib/utils/analytics'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
formatSwapSignedAnalyticsEventProperties,
|
||||
formatToDecimal,
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTransactionAdder } from 'state/transactions/hooks'
|
||||
import {
|
||||
@@ -19,6 +27,42 @@ import {
|
||||
WrapTransactionInfo,
|
||||
} from 'state/transactions/types'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
|
||||
interface AnalyticsEventProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
gasUsed: string | undefined
|
||||
blockNumber: number | undefined
|
||||
hash: string | undefined
|
||||
allowedSlippage: Percent
|
||||
succeeded: boolean
|
||||
}
|
||||
|
||||
const formatAnalyticsEventProperties = ({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
succeeded,
|
||||
gasUsed,
|
||||
blockNumber,
|
||||
}: AnalyticsEventProps) => ({
|
||||
estimated_network_fee_usd: gasUsed,
|
||||
transaction_hash: hash,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
swap_quote_block_number: blockNumber,
|
||||
succeeded,
|
||||
})
|
||||
|
||||
/** Integrates the Widget's transactions, showing the widget's transactions in the app. */
|
||||
export function useSyncWidgetTransactions() {
|
||||
@@ -46,7 +90,7 @@ export function useSyncWidgetTransactions() {
|
||||
amount: transactionAmount
|
||||
? formatToDecimal(transactionAmount, transactionAmount?.currency.decimals)
|
||||
: undefined,
|
||||
type: type === WidgetTransactionType.WRAP ? WrapType.WRAP : WrapType.UNWRAP,
|
||||
type: type === WidgetTransactionType.WRAP ? TransactionType.WRAP : TransactionType.UNWRAP,
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, eventProperties)
|
||||
@@ -94,7 +138,24 @@ export function useSyncWidgetTransactions() {
|
||||
[addTransaction, chainId, trace]
|
||||
)
|
||||
|
||||
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit }), [onTxSubmit])
|
||||
const onTxSuccess: OnTxSuccess = useCallback((hash: string, tx) => {
|
||||
if (tx.info.type === TransactionType.SWAP) {
|
||||
const { trade, slippageTolerance } = tx.info
|
||||
sendAnalyticsEvent(
|
||||
SwapEventName.SWAP_TRANSACTION_COMPLETED,
|
||||
formatAnalyticsEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
gasUsed: tx.receipt?.gasUsed?.toString(),
|
||||
blockNumber: tx.receipt?.blockNumber,
|
||||
allowedSlippage: slippageTolerance,
|
||||
succeeded: tx.receipt?.status === 1,
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit, onTxSuccess }), [onTxSubmit, onTxSuccess])
|
||||
|
||||
return { transactions: { ...txHandlers } }
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ interface L1ChainInfo extends BaseChainInfo {
|
||||
readonly defaultListUrl?: string
|
||||
}
|
||||
|
||||
interface L2ChainInfo extends BaseChainInfo {
|
||||
export interface L2ChainInfo extends BaseChainInfo {
|
||||
readonly networkType: NetworkType.L2
|
||||
readonly bridge: string
|
||||
readonly statusPage?: string
|
||||
|
||||
@@ -20,7 +20,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
|
||||
},
|
||||
}
|
||||
|
||||
export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 13
|
||||
// in PoS, ethereum block time is 12s, see https://ethereum.org/en/developers/docs/blocks/#block-time
|
||||
export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 12
|
||||
|
||||
// Block time here is slightly higher (~1s) than average in order to avoid ongoing proposals past the displayed time
|
||||
export const AVERAGE_BLOCK_TIME_IN_SECS: { [chainId: number]: number } = {
|
||||
|
||||
@@ -8,7 +8,6 @@ const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
|
||||
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
|
||||
const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json'
|
||||
const KLEROS_LIST = 't2crtokens.eth'
|
||||
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
|
||||
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
|
||||
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
|
||||
|
||||
@@ -30,7 +29,6 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
|
||||
GEMINI_LIST,
|
||||
WRAPPED_LIST,
|
||||
SET_LIST,
|
||||
ROLL_LIST,
|
||||
ARBITRUM_LIST,
|
||||
OPTIMISM_LIST,
|
||||
CELO_LIST,
|
||||
|
||||
@@ -4,4 +4,6 @@ export enum FeatureFlag {
|
||||
permit2 = 'permit2',
|
||||
nftListV2 = 'nftListV2',
|
||||
payWithAnyToken = 'payWithAnyToken',
|
||||
swapWidget = 'swapWidget',
|
||||
gqlRouting = 'gqlRouting',
|
||||
}
|
||||
|
||||
7
src/featureFlags/flags/gqlRouting.ts
Normal file
7
src/featureFlags/flags/gqlRouting.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useGqlRoutingFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.gqlRouting)
|
||||
}
|
||||
|
||||
export { BaseVariant as GqlRoutingVariant }
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
import { BaseVariant } from '../index'
|
||||
|
||||
export function useNftListV2Flag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.nftListV2)
|
||||
return BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as NftListV2Variant }
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function usePayWithAnyTokenFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.payWithAnyToken)
|
||||
}
|
||||
|
||||
export function usePayWithAnyTokenEnabled(): boolean {
|
||||
const flagEnabled = usePayWithAnyTokenFlag() === BaseVariant.Enabled
|
||||
const { chainId } = useWeb3React()
|
||||
try {
|
||||
// Detect if the Universal Router is not yet deployed to chainId.
|
||||
// This is necessary so that we can fallback correctly on chains without a Universal Router deployment.
|
||||
// It will be removed once Universal Router is deployed on all supported chains.
|
||||
chainId && UNIVERSAL_ROUTER_ADDRESS(chainId)
|
||||
return flagEnabled
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseVariant as PayWithAnyTokenVariant }
|
||||
|
||||
11
src/featureFlags/flags/swapWidget.ts
Normal file
11
src/featureFlags/flags/swapWidget.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useSwapWidgetFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.swapWidget, BaseVariant.Control)
|
||||
}
|
||||
|
||||
export function useSwapWidgetEnabled(): boolean {
|
||||
return useSwapWidgetFlag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as SwapWidgetVariant }
|
||||
@@ -14,8 +14,8 @@ The difference between Token and TokenProject:
|
||||
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
|
||||
*/
|
||||
gql`
|
||||
query Token($contract: ContractInput!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query Token($chain: Chain!, $address: String = null) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
@@ -23,31 +23,39 @@ gql`
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
id
|
||||
value
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
logoUrl
|
||||
tokens {
|
||||
id
|
||||
chain
|
||||
address
|
||||
}
|
||||
@@ -58,7 +66,7 @@ gql`
|
||||
|
||||
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
|
||||
|
||||
export type TokenQueryData = NonNullable<TokenQuery['tokens']>[number]
|
||||
export type TokenQueryData = TokenQuery['token']
|
||||
|
||||
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
|
||||
export class QueryToken extends WrappedTokenInfo {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support
|
||||
gql`
|
||||
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
|
||||
@@ -15,7 +15,15 @@ import {
|
||||
useTopTokens100Query,
|
||||
useTopTokensSparklineQuery,
|
||||
} from './__generated__/types-and-hooks'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, isPricePoint, PricePoint, toHistoryDuration, unwrapToken } from './util'
|
||||
import {
|
||||
CHAIN_NAME_TO_CHAIN_ID,
|
||||
isPricePoint,
|
||||
PollingInterval,
|
||||
PricePoint,
|
||||
toHistoryDuration,
|
||||
unwrapToken,
|
||||
usePollQueryWhileMounted,
|
||||
} from './util'
|
||||
|
||||
gql`
|
||||
query TopTokens100($duration: HistoryDuration!, $chain: Chain!) {
|
||||
@@ -26,24 +34,30 @@ gql`
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: $duration) {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
volume(duration: $duration) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
@@ -53,9 +67,13 @@ gql`
|
||||
gql`
|
||||
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -64,11 +82,12 @@ gql`
|
||||
}
|
||||
`
|
||||
|
||||
function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
function useSortedTokens(tokens: TopTokens100Query['topTokens']) {
|
||||
const sortMethod = useAtomValue(sortMethodAtom)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!tokens) return undefined
|
||||
let tokenArray = Array.from(tokens)
|
||||
switch (sortMethod) {
|
||||
case TokenSortMethod.PRICE:
|
||||
@@ -93,12 +112,13 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
}, [tokens, sortMethod, sortAscending])
|
||||
}
|
||||
|
||||
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
function useFilteredTokens(tokens: TopTokens100Query['topTokens']) {
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
|
||||
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
|
||||
|
||||
return useMemo(() => {
|
||||
if (!tokens) return undefined
|
||||
let returnTokens = tokens
|
||||
if (lowercaseFilterString) {
|
||||
returnTokens = returnTokens?.filter((token) => {
|
||||
@@ -119,7 +139,7 @@ export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[
|
||||
|
||||
interface UseTopTokensReturnValue {
|
||||
tokens: TopToken[] | undefined
|
||||
tokenVolumeRank: Record<string, number>
|
||||
tokenSortRank: Record<string, number>
|
||||
loadingTokens: boolean
|
||||
sparklines: SparklineMap
|
||||
}
|
||||
@@ -128,9 +148,12 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
|
||||
|
||||
const { data: sparklineQuery } = useTopTokensSparklineQuery({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
const { data: sparklineQuery } = usePollQueryWhileMounted(
|
||||
useTopTokensSparklineQuery({
|
||||
variables: { duration, chain },
|
||||
}),
|
||||
PollingInterval.Slow
|
||||
)
|
||||
|
||||
const sparklines = useMemo(() => {
|
||||
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
|
||||
@@ -141,33 +164,29 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
return map
|
||||
}, [chainId, sparklineQuery?.topTokens])
|
||||
|
||||
const { data, loading: loadingTokens } = useTopTokens100Query({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
const unwrappedTokens = useMemo(
|
||||
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
|
||||
[chainId, data]
|
||||
const { data, loading: loadingTokens } = usePollQueryWhileMounted(
|
||||
useTopTokens100Query({
|
||||
variables: { duration, chain },
|
||||
}),
|
||||
PollingInterval.Fast
|
||||
)
|
||||
const tokenVolumeRank = useMemo(
|
||||
|
||||
const unwrappedTokens = useMemo(() => data?.topTokens?.map((token) => unwrapToken(chainId, token)), [chainId, data])
|
||||
const sortedTokens = useSortedTokens(unwrappedTokens)
|
||||
const tokenSortRank = useMemo(
|
||||
() =>
|
||||
unwrappedTokens
|
||||
.sort((a, b) => {
|
||||
if (!a.market?.volume || !b.market?.volume) return 0
|
||||
return a.market.volume.value > b.market.volume.value ? -1 : 1
|
||||
})
|
||||
.reduce((acc, cur, i) => {
|
||||
if (!cur.address) return acc
|
||||
return {
|
||||
...acc,
|
||||
[cur.address]: i + 1,
|
||||
}
|
||||
}, {}),
|
||||
[unwrappedTokens]
|
||||
sortedTokens?.reduce((acc, cur, i) => {
|
||||
if (!cur.address) return acc
|
||||
return {
|
||||
...acc,
|
||||
[cur.address]: i + 1,
|
||||
}
|
||||
}, {}) ?? {},
|
||||
[sortedTokens]
|
||||
)
|
||||
const filteredTokens = useFilteredTokens(unwrappedTokens)
|
||||
const sortedTokens = useSortedTokens(filteredTokens)
|
||||
const filteredTokens = useFilteredTokens(sortedTokens)
|
||||
return useMemo(
|
||||
() => ({ tokens: sortedTokens, tokenVolumeRank, loadingTokens, sparklines }),
|
||||
[loadingTokens, tokenVolumeRank, sortedTokens, sparklines]
|
||||
() => ({ tokens: filteredTokens, tokenSortRank, loadingTokens, sparklines }),
|
||||
[filteredTokens, tokenSortRank, loadingTokens, sparklines]
|
||||
)
|
||||
}
|
||||
|
||||
246
src/graphql/data/__generated__/types-and-hooks.ts
generated
246
src/graphql/data/__generated__/types-and-hooks.ts
generated
@@ -386,6 +386,7 @@ export type NftCollectionTraitStats = {
|
||||
|
||||
export type NftCollectionsFilterInput = {
|
||||
addresses?: InputMaybe<Array<Scalars['String']>>;
|
||||
nameQuery?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type NftContract = IContract & {
|
||||
@@ -468,12 +469,45 @@ export enum NftRarityProvider {
|
||||
RaritySniper = 'RARITY_SNIPER'
|
||||
}
|
||||
|
||||
export type NftRouteResponse = {
|
||||
__typename?: 'NftRouteResponse';
|
||||
calldata: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
route?: Maybe<Array<NftTrade>>;
|
||||
sendAmount: TokenAmount;
|
||||
toAddress: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum NftStandard {
|
||||
Erc721 = 'ERC721',
|
||||
Erc1155 = 'ERC1155',
|
||||
Noncompliant = 'NONCOMPLIANT'
|
||||
}
|
||||
|
||||
export type NftTrade = {
|
||||
__typename?: 'NftTrade';
|
||||
amount: Scalars['Int'];
|
||||
contractAddress: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
marketplace: NftMarketplace;
|
||||
/** price represents the current price of the NFT, which can be different from quotePrice */
|
||||
price: TokenAmount;
|
||||
/** quotePrice represents the last quoted price of the NFT */
|
||||
quotePrice?: Maybe<TokenAmount>;
|
||||
tokenId: Scalars['String'];
|
||||
tokenType: NftStandard;
|
||||
};
|
||||
|
||||
export type NftTradeInput = {
|
||||
amount: Scalars['Int'];
|
||||
contractAddress: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
marketplace: NftMarketplace;
|
||||
quotePrice?: InputMaybe<TokenAmountInput>;
|
||||
tokenId: Scalars['String'];
|
||||
tokenType: NftStandard;
|
||||
};
|
||||
|
||||
export type NftTransfer = {
|
||||
__typename?: 'NftTransfer';
|
||||
asset: NftAsset;
|
||||
@@ -504,6 +538,22 @@ export type PageInfo = {
|
||||
startCursor?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
/** v2 pool parameters as defined by https://github.com/Uniswap/v2-sdk/blob/main/src/entities/pair.ts */
|
||||
export type PairInput = {
|
||||
tokenAmountA: TokenAmountInput;
|
||||
tokenAmountB: TokenAmountInput;
|
||||
};
|
||||
|
||||
/** v3 pool parameters as defined by https://github.com/Uniswap/v3-sdk/blob/main/src/entities/pool.ts */
|
||||
export type PoolInput = {
|
||||
fee: Scalars['Int'];
|
||||
liquidity: Scalars['String'];
|
||||
sqrtRatioX96: Scalars['String'];
|
||||
tickCurrent: Scalars['String'];
|
||||
tokenA: TokenInput;
|
||||
tokenB: TokenInput;
|
||||
};
|
||||
|
||||
export type Portfolio = {
|
||||
__typename?: 'Portfolio';
|
||||
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
|
||||
@@ -529,11 +579,11 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
|
||||
nftAssets?: Maybe<NftAssetConnection>;
|
||||
nftBalances?: Maybe<NftBalanceConnection>;
|
||||
nftCollections?: Maybe<NftCollectionConnection>;
|
||||
nftCollectionsById?: Maybe<Array<Maybe<NftCollection>>>;
|
||||
nftRoute?: Maybe<NftRouteResponse>;
|
||||
portfolios?: Maybe<Array<Maybe<Portfolio>>>;
|
||||
searchTokenProjects?: Maybe<Array<Maybe<TokenProject>>>;
|
||||
searchTokens?: Maybe<Array<Maybe<Token>>>;
|
||||
@@ -544,13 +594,6 @@ export type Query = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryAssetActivitiesArgs = {
|
||||
address: Scalars['String'];
|
||||
page?: InputMaybe<Scalars['Int']>;
|
||||
pageSize?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryNftAssetsArgs = {
|
||||
address: Scalars['String'];
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
@@ -589,6 +632,14 @@ export type QueryNftCollectionsByIdArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryNftRouteArgs = {
|
||||
chain?: InputMaybe<Chain>;
|
||||
nftTrades: Array<NftTradeInput>;
|
||||
senderAddress: Scalars['String'];
|
||||
tokenTrades?: InputMaybe<Array<TokenTradeInput>>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryPortfoliosArgs = {
|
||||
ownerAddresses: Array<Scalars['String']>;
|
||||
useAltDataSource?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -661,6 +712,18 @@ export type TokenMarketArgs = {
|
||||
currency?: InputMaybe<Currency>;
|
||||
};
|
||||
|
||||
export type TokenAmount = {
|
||||
__typename?: 'TokenAmount';
|
||||
currency: Currency;
|
||||
id: Scalars['ID'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TokenAmountInput = {
|
||||
amount: Scalars['String'];
|
||||
token: TokenInput;
|
||||
};
|
||||
|
||||
export type TokenApproval = {
|
||||
__typename?: 'TokenApproval';
|
||||
approvedAddress: Scalars['String'];
|
||||
@@ -683,6 +746,13 @@ export type TokenBalance = {
|
||||
tokenProjectMarket?: Maybe<TokenProjectMarket>;
|
||||
};
|
||||
|
||||
export type TokenInput = {
|
||||
address: Scalars['String'];
|
||||
chainId: Scalars['Int'];
|
||||
decimals: Scalars['Int'];
|
||||
isNative: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type TokenMarket = {
|
||||
__typename?: 'TokenMarket';
|
||||
id: Scalars['ID'];
|
||||
@@ -788,6 +858,30 @@ export enum TokenStandard {
|
||||
Native = 'NATIVE'
|
||||
}
|
||||
|
||||
export type TokenTradeInput = {
|
||||
routes?: InputMaybe<TokenTradeRoutesInput>;
|
||||
slippageToleranceBasisPoints?: InputMaybe<Scalars['Int']>;
|
||||
tokenAmount: TokenAmountInput;
|
||||
};
|
||||
|
||||
export type TokenTradeRouteInput = {
|
||||
inputAmount: TokenAmountInput;
|
||||
outputAmount: TokenAmountInput;
|
||||
pools: Array<TradePoolInput>;
|
||||
};
|
||||
|
||||
export type TokenTradeRoutesInput = {
|
||||
mixedRoutes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
tradeType: TokenTradeType;
|
||||
v2Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
v3Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
};
|
||||
|
||||
export enum TokenTradeType {
|
||||
ExactInput = 'EXACT_INPUT',
|
||||
ExactOutput = 'EXACT_OUTPUT'
|
||||
}
|
||||
|
||||
export type TokenTransfer = {
|
||||
__typename?: 'TokenTransfer';
|
||||
asset: Token;
|
||||
@@ -800,6 +894,11 @@ export type TokenTransfer = {
|
||||
transactedValue?: Maybe<Amount>;
|
||||
};
|
||||
|
||||
export type TradePoolInput = {
|
||||
pair?: InputMaybe<PairInput>;
|
||||
pool?: InputMaybe<PoolInput>;
|
||||
};
|
||||
|
||||
export type Transaction = {
|
||||
__typename?: 'Transaction';
|
||||
blockNumber: Scalars['Int'];
|
||||
@@ -826,19 +925,21 @@ export enum TransactionStatus {
|
||||
}
|
||||
|
||||
export type TokenQueryVariables = Exact<{
|
||||
contract: ContractInput;
|
||||
chain: Chain;
|
||||
address?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TokenQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', value: number }, priceLow52W?: { __typename?: 'Amount', value: number } }, project?: { __typename?: 'TokenProject', description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', chain: Chain, address?: string }> } }> };
|
||||
export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', id: string, value: number }, priceLow52W?: { __typename?: 'Amount', id: string, value: number } }, project?: { __typename?: 'TokenProject', id: string, description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', id: string, chain: Chain, address?: string }> } } };
|
||||
|
||||
export type TokenPriceQueryVariables = Exact<{
|
||||
contract: ContractInput;
|
||||
chain: Chain;
|
||||
address?: InputMaybe<Scalars['String']>;
|
||||
duration: HistoryDuration;
|
||||
}>;
|
||||
|
||||
|
||||
export type TokenPriceQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', market?: { __typename?: 'TokenMarket', price?: { __typename?: 'Amount', value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
|
||||
export type TokenPriceQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } } };
|
||||
|
||||
export type TopTokens100QueryVariables = Exact<{
|
||||
duration: HistoryDuration;
|
||||
@@ -846,7 +947,7 @@ export type TopTokens100QueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', currency?: Currency, value: number }, volume?: { __typename?: 'Amount', value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', logoUrl?: string } }> };
|
||||
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, currency?: Currency, value: number }, volume?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string } }> };
|
||||
|
||||
export type TopTokensSparklineQueryVariables = Exact<{
|
||||
duration: HistoryDuration;
|
||||
@@ -854,7 +955,7 @@ export type TopTokensSparklineQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', address?: string, market?: { __typename?: 'TokenMarket', priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
|
||||
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } }> };
|
||||
|
||||
export type AssetQueryVariables = Exact<{
|
||||
address: Scalars['String'];
|
||||
@@ -895,12 +996,22 @@ export type NftBalanceQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
|
||||
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, twitterName?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
|
||||
|
||||
export type NftRouteQueryVariables = Exact<{
|
||||
chain?: InputMaybe<Chain>;
|
||||
senderAddress: Scalars['String'];
|
||||
nftTrades: Array<NftTradeInput> | NftTradeInput;
|
||||
tokenTrades?: InputMaybe<Array<TokenTradeInput> | TokenTradeInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', currency: Currency, value: string } } };
|
||||
|
||||
|
||||
export const TokenDocument = gql`
|
||||
query Token($contract: ContractInput!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query Token($chain: Chain!, $address: String = null) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
@@ -908,31 +1019,39 @@ export const TokenDocument = gql`
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
id
|
||||
value
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
logoUrl
|
||||
tokens {
|
||||
id
|
||||
chain
|
||||
address
|
||||
}
|
||||
@@ -953,7 +1072,8 @@ export const TokenDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useTokenQuery({
|
||||
* variables: {
|
||||
* contract: // value for 'contract'
|
||||
* chain: // value for 'chain'
|
||||
* address: // value for 'address'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
@@ -969,13 +1089,19 @@ export type TokenQueryHookResult = ReturnType<typeof useTokenQuery>;
|
||||
export type TokenLazyQueryHookResult = ReturnType<typeof useTokenLazyQuery>;
|
||||
export type TokenQueryResult = Apollo.QueryResult<TokenQuery, TokenQueryVariables>;
|
||||
export const TokenPriceDocument = gql`
|
||||
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -996,7 +1122,8 @@ export const TokenPriceDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useTokenPriceQuery({
|
||||
* variables: {
|
||||
* contract: // value for 'contract'
|
||||
* chain: // value for 'chain'
|
||||
* address: // value for 'address'
|
||||
* duration: // value for 'duration'
|
||||
* },
|
||||
* });
|
||||
@@ -1021,24 +1148,30 @@ export const TopTokens100Document = gql`
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: $duration) {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
volume(duration: $duration) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
@@ -1076,9 +1209,13 @@ export type TopTokens100QueryResult = Apollo.QueryResult<TopTokens100Query, TopT
|
||||
export const TopTokensSparklineDocument = gql`
|
||||
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -1487,6 +1624,7 @@ export const NftBalanceDocument = gql`
|
||||
url
|
||||
}
|
||||
name
|
||||
twitterName
|
||||
nftContracts {
|
||||
address
|
||||
chain
|
||||
@@ -1586,4 +1724,68 @@ export function useNftBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
|
||||
}
|
||||
export type NftBalanceQueryHookResult = ReturnType<typeof useNftBalanceQuery>;
|
||||
export type NftBalanceLazyQueryHookResult = ReturnType<typeof useNftBalanceLazyQuery>;
|
||||
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
|
||||
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
|
||||
export const NftRouteDocument = gql`
|
||||
query NftRoute($chain: Chain = ETHEREUM, $senderAddress: String!, $nftTrades: [NftTradeInput!]!, $tokenTrades: [TokenTradeInput!]) {
|
||||
nftRoute(
|
||||
chain: $chain
|
||||
senderAddress: $senderAddress
|
||||
nftTrades: $nftTrades
|
||||
tokenTrades: $tokenTrades
|
||||
) {
|
||||
calldata
|
||||
route {
|
||||
amount
|
||||
contractAddress
|
||||
id
|
||||
marketplace
|
||||
price {
|
||||
currency
|
||||
value
|
||||
}
|
||||
quotePrice {
|
||||
currency
|
||||
value
|
||||
}
|
||||
tokenId
|
||||
tokenType
|
||||
}
|
||||
sendAmount {
|
||||
currency
|
||||
value
|
||||
}
|
||||
toAddress
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useNftRouteQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useNftRouteQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useNftRouteQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useNftRouteQuery({
|
||||
* variables: {
|
||||
* chain: // value for 'chain'
|
||||
* senderAddress: // value for 'senderAddress'
|
||||
* nftTrades: // value for 'nftTrades'
|
||||
* tokenTrades: // value for 'tokenTrades'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useNftRouteQuery(baseOptions: Apollo.QueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
|
||||
}
|
||||
export function useNftRouteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
|
||||
}
|
||||
export type NftRouteQueryHookResult = ReturnType<typeof useNftRouteQuery>;
|
||||
export type NftRouteLazyQueryHookResult = ReturnType<typeof useNftRouteLazyQuery>;
|
||||
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
||||
import { relayStylePagination } from '@apollo/client/utilities'
|
||||
import { Reference, relayStylePagination } from '@apollo/client/utilities'
|
||||
|
||||
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
|
||||
if (!GRAPHQL_URL) {
|
||||
@@ -7,6 +7,7 @@ if (!GRAPHQL_URL) {
|
||||
}
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
uri: GRAPHQL_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -18,6 +19,33 @@ export const apolloClient = new ApolloClient({
|
||||
fields: {
|
||||
nftBalances: relayStylePagination(),
|
||||
nftAssets: relayStylePagination(),
|
||||
// tell apollo client how to reference Token items in the cache after being fetched by queries that return Token[]
|
||||
token: {
|
||||
read(_, { args, toReference }): Reference | undefined {
|
||||
return toReference({
|
||||
__typename: 'Token',
|
||||
chain: args?.chain,
|
||||
address: args?.address,
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Token: {
|
||||
// key by chain, address combination so that Token(chain, address) endpoint can read from cache
|
||||
/**
|
||||
* NOTE: In any query for `token` or `tokens`, you must include the `chain` and `address` fields
|
||||
* in order for result to normalize properly in the cache.
|
||||
*/
|
||||
keyFields: ['chain', 'address'],
|
||||
fields: {
|
||||
address: {
|
||||
read(address: string | null): string | null {
|
||||
// backend endpoint sometimes returns checksummed, sometimes lowercased addresses
|
||||
// always use lowercased addresses in our app for consistency
|
||||
return address?.toLowerCase() ?? null
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@ gql`
|
||||
url
|
||||
}
|
||||
name
|
||||
twitterName
|
||||
nftContracts {
|
||||
address
|
||||
chain
|
||||
@@ -166,7 +167,12 @@ export function useNftBalance(
|
||||
image_url: asset?.collection?.image?.url,
|
||||
payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress,
|
||||
},
|
||||
collection: asset?.collection as unknown as GenieCollection,
|
||||
collection: {
|
||||
name: asset.collection?.name,
|
||||
isVerified: asset.collection?.isVerified,
|
||||
imageUrl: asset.collection?.image?.url,
|
||||
twitterUrl: asset.collection?.twitterName ? `@${asset.collection?.twitterName}` : undefined,
|
||||
} as GenieCollection,
|
||||
collectionIsVerified: asset?.collection?.isVerified,
|
||||
lastPrice: queryAsset.node.lastPrice?.value,
|
||||
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,
|
||||
|
||||
47
src/graphql/data/nft/Routing.ts
Normal file
47
src/graphql/data/nft/Routing.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { NftTradeInput, TokenTradeInput, useNftRouteQuery } from '../__generated__/types-and-hooks'
|
||||
|
||||
gql`
|
||||
query NftRoute(
|
||||
$chain: Chain = ETHEREUM
|
||||
$senderAddress: String!
|
||||
$nftTrades: [NftTradeInput!]!
|
||||
$tokenTrades: [TokenTradeInput!]
|
||||
) {
|
||||
nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) {
|
||||
calldata
|
||||
route {
|
||||
amount
|
||||
contractAddress
|
||||
id
|
||||
marketplace
|
||||
price {
|
||||
currency
|
||||
value
|
||||
}
|
||||
quotePrice {
|
||||
currency
|
||||
value
|
||||
}
|
||||
tokenId
|
||||
tokenType
|
||||
}
|
||||
sendAmount {
|
||||
currency
|
||||
value
|
||||
}
|
||||
toAddress
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function useNftRoute(senderAddress: string, nftTrades: NftTradeInput[], tokenTrades?: TokenTradeInput[]) {
|
||||
return useNftRouteQuery({
|
||||
variables: {
|
||||
senderAddress,
|
||||
nftTrades,
|
||||
tokenTrades,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,31 @@
|
||||
import { QueryResult } from '@apollo/client'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { ZERO_ADDRESS } from 'constants/misc'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { Chain, HistoryDuration } from './__generated__/types-and-hooks'
|
||||
|
||||
export enum PollingInterval {
|
||||
Slow = ms`5m`,
|
||||
Normal = ms`1m`,
|
||||
Fast = ms`12s`, // 12 seconds, block times for mainnet
|
||||
LightningMcQueen = ms`3s`, // 3 seconds, approx block times for polygon
|
||||
}
|
||||
|
||||
// Polls a query only when the current component is mounted, as useQuery's pollInterval prop will continue to poll after unmount
|
||||
export function usePollQueryWhileMounted<T, K>(queryResult: QueryResult<T, K>, interval: PollingInterval) {
|
||||
const { startPolling, stopPolling } = queryResult
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(interval)
|
||||
return stopPolling
|
||||
}, [interval, startPolling, stopPolling])
|
||||
|
||||
return queryResult
|
||||
}
|
||||
|
||||
export enum TimePeriod {
|
||||
HOUR,
|
||||
DAY,
|
||||
|
||||
@@ -37,6 +37,7 @@ export type Bundle = {
|
||||
export type Bundle_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Bundle_Filter>>>;
|
||||
ethPriceUSD?: InputMaybe<Scalars['BigDecimal']>;
|
||||
ethPriceUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
ethPriceUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -53,6 +54,7 @@ export type Bundle_Filter = {
|
||||
id_lte?: InputMaybe<Scalars['ID']>;
|
||||
id_not?: InputMaybe<Scalars['ID']>;
|
||||
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Bundle_Filter>>>;
|
||||
};
|
||||
|
||||
export enum Bundle_OrderBy {
|
||||
@@ -114,6 +116,7 @@ export type Burn_Filter = {
|
||||
amount_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
amount_not?: InputMaybe<Scalars['BigInt']>;
|
||||
amount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Burn_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -130,6 +133,7 @@ export type Burn_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Burn_Filter>>>;
|
||||
origin?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
@@ -320,6 +324,7 @@ export type Collect_Filter = {
|
||||
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Collect_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -336,6 +341,7 @@ export type Collect_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Collect_Filter>>>;
|
||||
owner?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
@@ -448,6 +454,7 @@ export type Factory = {
|
||||
export type Factory_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Factory_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -456,6 +463,7 @@ export type Factory_Filter = {
|
||||
id_lte?: InputMaybe<Scalars['ID']>;
|
||||
id_not?: InputMaybe<Scalars['ID']>;
|
||||
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Factory_Filter>>>;
|
||||
owner?: InputMaybe<Scalars['ID']>;
|
||||
owner_gt?: InputMaybe<Scalars['ID']>;
|
||||
owner_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -629,6 +637,7 @@ export type Flash_Filter = {
|
||||
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Flash_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -645,6 +654,7 @@ export type Flash_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Flash_Filter>>>;
|
||||
pool?: InputMaybe<Scalars['String']>;
|
||||
pool_?: InputMaybe<Pool_Filter>;
|
||||
pool_contains?: InputMaybe<Scalars['String']>;
|
||||
@@ -787,6 +797,7 @@ export type Mint_Filter = {
|
||||
amount_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
amount_not?: InputMaybe<Scalars['BigInt']>;
|
||||
amount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Mint_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -803,6 +814,7 @@ export type Mint_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Mint_Filter>>>;
|
||||
origin?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
@@ -1098,6 +1110,7 @@ export type PoolDayData = {
|
||||
export type PoolDayData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<PoolDayData_Filter>>>;
|
||||
close?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -1178,6 +1191,7 @@ export type PoolDayData_Filter = {
|
||||
open_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<PoolDayData_Filter>>>;
|
||||
pool?: InputMaybe<Scalars['String']>;
|
||||
pool_?: InputMaybe<Pool_Filter>;
|
||||
pool_contains?: InputMaybe<Scalars['String']>;
|
||||
@@ -1323,6 +1337,7 @@ export type PoolHourData = {
|
||||
export type PoolHourData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<PoolHourData_Filter>>>;
|
||||
close?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -1395,6 +1410,7 @@ export type PoolHourData_Filter = {
|
||||
open_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<PoolHourData_Filter>>>;
|
||||
periodStartUnix?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -1524,6 +1540,7 @@ export enum PoolHourData_OrderBy {
|
||||
export type Pool_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Pool_Filter>>>;
|
||||
burns_?: InputMaybe<Burn_Filter>;
|
||||
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -1631,6 +1648,7 @@ export type Pool_Filter = {
|
||||
observationIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
observationIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
observationIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Pool_Filter>>>;
|
||||
poolDayData_?: InputMaybe<PoolDayData_Filter>;
|
||||
poolHourData_?: InputMaybe<PoolHourData_Filter>;
|
||||
sqrtPrice?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -1874,6 +1892,7 @@ export type PositionSnapshot = {
|
||||
export type PositionSnapshot_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<PositionSnapshot_Filter>>>;
|
||||
blockNumber?: InputMaybe<Scalars['BigInt']>;
|
||||
blockNumber_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
blockNumber_gte?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -1946,6 +1965,7 @@ export type PositionSnapshot_Filter = {
|
||||
liquidity_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidity_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<PositionSnapshot_Filter>>>;
|
||||
owner?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
@@ -2067,6 +2087,7 @@ export enum PositionSnapshot_OrderBy {
|
||||
export type Position_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Position_Filter>>>;
|
||||
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -2131,6 +2152,7 @@ export type Position_Filter = {
|
||||
liquidity_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidity_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Position_Filter>>>;
|
||||
owner?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
@@ -3173,6 +3195,7 @@ export type Swap_Filter = {
|
||||
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Swap_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -3189,6 +3212,7 @@ export type Swap_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Swap_Filter>>>;
|
||||
origin?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
@@ -3391,6 +3415,7 @@ export type TickDayData = {
|
||||
export type TickDayData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<TickDayData_Filter>>>;
|
||||
date?: InputMaybe<Scalars['Int']>;
|
||||
date_gt?: InputMaybe<Scalars['Int']>;
|
||||
date_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -3447,6 +3472,7 @@ export type TickDayData_Filter = {
|
||||
liquidityNet_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityNet_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityNet_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<TickDayData_Filter>>>;
|
||||
pool?: InputMaybe<Scalars['String']>;
|
||||
pool_?: InputMaybe<Pool_Filter>;
|
||||
pool_contains?: InputMaybe<Scalars['String']>;
|
||||
@@ -3547,6 +3573,7 @@ export type TickHourData = {
|
||||
export type TickHourData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<TickHourData_Filter>>>;
|
||||
feesUSD?: InputMaybe<Scalars['BigDecimal']>;
|
||||
feesUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
feesUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -3579,6 +3606,7 @@ export type TickHourData_Filter = {
|
||||
liquidityNet_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityNet_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityNet_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<TickHourData_Filter>>>;
|
||||
periodStartUnix?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -3671,6 +3699,7 @@ export enum TickHourData_OrderBy {
|
||||
export type Tick_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Tick_Filter>>>;
|
||||
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -3767,6 +3796,7 @@ export type Tick_Filter = {
|
||||
liquidityProviderCount_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityProviderCount_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityProviderCount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Tick_Filter>>>;
|
||||
pool?: InputMaybe<Scalars['String']>;
|
||||
poolAddress?: InputMaybe<Scalars['String']>;
|
||||
poolAddress_contains?: InputMaybe<Scalars['String']>;
|
||||
@@ -3950,6 +3980,7 @@ export type TokenDayData = {
|
||||
export type TokenDayData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<TokenDayData_Filter>>>;
|
||||
close?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -4006,6 +4037,7 @@ export type TokenDayData_Filter = {
|
||||
open_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<TokenDayData_Filter>>>;
|
||||
priceUSD?: InputMaybe<Scalars['BigDecimal']>;
|
||||
priceUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
priceUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -4115,6 +4147,7 @@ export type TokenHourData = {
|
||||
export type TokenHourData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<TokenHourData_Filter>>>;
|
||||
close?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -4163,6 +4196,7 @@ export type TokenHourData_Filter = {
|
||||
open_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<TokenHourData_Filter>>>;
|
||||
periodStartUnix?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -4262,6 +4296,7 @@ export enum TokenHourData_OrderBy {
|
||||
export type Token_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Token_Filter>>>;
|
||||
decimals?: InputMaybe<Scalars['BigInt']>;
|
||||
decimals_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
decimals_gte?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -4314,6 +4349,7 @@ export type Token_Filter = {
|
||||
name_not_starts_with_nocase?: InputMaybe<Scalars['String']>;
|
||||
name_starts_with?: InputMaybe<Scalars['String']>;
|
||||
name_starts_with_nocase?: InputMaybe<Scalars['String']>;
|
||||
or?: InputMaybe<Array<InputMaybe<Token_Filter>>>;
|
||||
poolCount?: InputMaybe<Scalars['BigInt']>;
|
||||
poolCount_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
poolCount_gte?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -4498,6 +4534,7 @@ export type TransactionSwapsArgs = {
|
||||
export type Transaction_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Transaction_Filter>>>;
|
||||
blockNumber?: InputMaybe<Scalars['BigInt']>;
|
||||
blockNumber_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
blockNumber_gte?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -4534,6 +4571,7 @@ export type Transaction_Filter = {
|
||||
id_not?: InputMaybe<Scalars['ID']>;
|
||||
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
|
||||
mints_?: InputMaybe<Mint_Filter>;
|
||||
or?: InputMaybe<Array<InputMaybe<Transaction_Filter>>>;
|
||||
swaps_?: InputMaybe<Swap_Filter>;
|
||||
timestamp?: InputMaybe<Scalars['BigInt']>;
|
||||
timestamp_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -4573,6 +4611,7 @@ export type UniswapDayData = {
|
||||
export type UniswapDayData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<UniswapDayData_Filter>>>;
|
||||
date?: InputMaybe<Scalars['Int']>;
|
||||
date_gt?: InputMaybe<Scalars['Int']>;
|
||||
date_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -4597,6 +4636,7 @@ export type UniswapDayData_Filter = {
|
||||
id_lte?: InputMaybe<Scalars['ID']>;
|
||||
id_not?: InputMaybe<Scalars['ID']>;
|
||||
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<UniswapDayData_Filter>>>;
|
||||
tvlUSD?: InputMaybe<Scalars['BigDecimal']>;
|
||||
tvlUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
tvlUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
|
||||
6
src/hooks/useIsLandingPage.ts
Normal file
6
src/hooks/useIsLandingPage.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
export function useIsLandingPage() {
|
||||
const { pathname } = useLocation()
|
||||
return pathname.endsWith('/')
|
||||
}
|
||||
16
src/hooks/useMachineTime.ts
Normal file
16
src/hooks/useMachineTime.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
const useMachineTimeMs = (updateInterval: number): number => {
|
||||
const [now, setNow] = useState(Date.now())
|
||||
|
||||
useInterval(
|
||||
useCallback(() => {
|
||||
setNow(Date.now())
|
||||
}, []),
|
||||
updateInterval
|
||||
)
|
||||
return now
|
||||
}
|
||||
|
||||
export default useMachineTimeMs
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'utils/signTypedData'
|
||||
|
||||
import { signTypedData } from '@uniswap/conedison/provider'
|
||||
import { AllowanceTransfer, MaxAllowanceTransferAmount, PERMIT2_ADDRESS, PermitSingle } from '@uniswap/permit2-sdk'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
@@ -77,7 +76,8 @@ export function useUpdatePermitAllowance(
|
||||
}
|
||||
|
||||
const { domain, types, values } = AllowanceTransfer.getPermitData(permit, PERMIT2_ADDRESS, chainId)
|
||||
const signature = await provider.getSigner(account)._signTypedData(domain, types, values)
|
||||
// Use conedison's signTypedData for better x-wallet compatibility.
|
||||
const signature = await signTypedData(provider.getSigner(account), domain, types, values)
|
||||
onPermitSignature?.({ ...permit, signature })
|
||||
return
|
||||
} catch (e: unknown) {
|
||||
|
||||
@@ -48,7 +48,12 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
|
||||
}, [currency, stablecoin, trade])
|
||||
|
||||
const lastPrice = useRef(price)
|
||||
if (!price || !lastPrice.current || !price.equalTo(lastPrice.current)) {
|
||||
if (
|
||||
!price ||
|
||||
!lastPrice.current ||
|
||||
!price.equalTo(lastPrice.current) ||
|
||||
!price.baseCurrency.equals(lastPrice.current.baseCurrency)
|
||||
) {
|
||||
lastPrice.current = price
|
||||
}
|
||||
return lastPrice.current
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { formatEther } from '@ethersproject/units'
|
||||
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 { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||
import { useNftRoute } from 'graphql/data/nft/Routing'
|
||||
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
|
||||
import { BagFooter } from 'nft/components/bag/BagFooter'
|
||||
import ListingModal from 'nft/components/bag/profile/ListingModal'
|
||||
@@ -24,14 +25,13 @@ import { fetchRoute } from 'nft/queries'
|
||||
import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
|
||||
import {
|
||||
buildSellObject,
|
||||
fetchPrice,
|
||||
formatAssetEventProperties,
|
||||
recalculateBagUsingPooledAssets,
|
||||
sortUpdatedAssets,
|
||||
} from 'nft/utils'
|
||||
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useQuery, useQueryClient } from 'react-query'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import shallow from 'zustand/shallow'
|
||||
@@ -127,7 +127,6 @@ const Bag = () => {
|
||||
bagExpanded,
|
||||
toggleBag,
|
||||
setTotalEthPrice,
|
||||
setTotalUsdPrice,
|
||||
setBagExpanded,
|
||||
} = useBag((state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }), shallow)
|
||||
const { uncheckedItemsInBag } = useBag(({ itemsInBag }) => ({ uncheckedItemsInBag: itemsInBag }))
|
||||
@@ -137,6 +136,7 @@ const Bag = () => {
|
||||
const isNFTPage = useIsNftPage()
|
||||
const isMobile = useIsMobile()
|
||||
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
|
||||
const usingGqlRouting = useGqlRoutingFlag() === GqlRoutingVariant.Enabled
|
||||
|
||||
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
|
||||
const transactionState = useSendTransaction((state) => state.state)
|
||||
@@ -158,9 +158,7 @@ const Bag = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {})
|
||||
|
||||
const { totalEthPrice, totalUsdPrice } = useMemo(() => {
|
||||
const { totalEthPrice } = useMemo(() => {
|
||||
const totalEthPrice = itemsInBag.reduce(
|
||||
(total, item) =>
|
||||
item.status !== BagItemStatus.UNAVAILABLE
|
||||
@@ -172,10 +170,9 @@ const Bag = () => {
|
||||
: total,
|
||||
BigNumber.from(0)
|
||||
)
|
||||
const totalUsdPrice = fetchedPriceData ? parseFloat(formatEther(totalEthPrice)) * fetchedPriceData : undefined
|
||||
|
||||
return { totalEthPrice, totalUsdPrice }
|
||||
}, [itemsInBag, fetchedPriceData])
|
||||
return { totalEthPrice }
|
||||
}, [itemsInBag])
|
||||
|
||||
const purchaseAssets = async (routingData: RouteResponse) => {
|
||||
if (!provider || !routingData) return
|
||||
@@ -200,6 +197,7 @@ const Bag = () => {
|
||||
setBagExpanded({ bagExpanded: false, manualClose: true })
|
||||
}, [setBagExpanded])
|
||||
|
||||
useNftRoute(usingGqlRouting ? account ?? '' : '', [])
|
||||
const fetchAssets = async () => {
|
||||
const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
|
||||
const ethSellObject = buildSellObject(
|
||||
@@ -283,8 +281,7 @@ const Bag = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setTotalEthPrice(totalEthPrice)
|
||||
setTotalUsdPrice(totalUsdPrice)
|
||||
}, [totalEthPrice, totalUsdPrice, setTotalEthPrice, setTotalUsdPrice])
|
||||
}, [totalEthPrice, setTotalEthPrice])
|
||||
|
||||
const hasAssetsToShow = itemsInBag.length > 0
|
||||
|
||||
@@ -305,10 +302,9 @@ const Bag = () => {
|
||||
|
||||
const eventProperties = useMemo(
|
||||
() => ({
|
||||
usd_value: totalUsdPrice,
|
||||
...formatAssetEventProperties(itemsInBag.map((item) => item.asset)),
|
||||
}),
|
||||
[itemsInBag, totalUsdPrice]
|
||||
[itemsInBag]
|
||||
)
|
||||
|
||||
if (!bagExpanded || !isNFTPage) {
|
||||
@@ -334,7 +330,6 @@ const Bag = () => {
|
||||
{hasAssetsToShow && !isProfilePage && (
|
||||
<BagFooter
|
||||
totalEthPrice={totalEthPrice}
|
||||
totalUsdPrice={totalUsdPrice}
|
||||
bagStatus={bagStatus}
|
||||
fetchAssets={fetchAssets}
|
||||
eventProperties={eventProperties}
|
||||
|
||||
@@ -1,30 +1,44 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { formatEther } from '@ethersproject/units'
|
||||
import { parseEther } from '@ethersproject/units'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { formatPriceImpact } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import Loader from 'components/Loader'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row from 'components/Row'
|
||||
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useBag } from 'nft/hooks/useBag'
|
||||
import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap'
|
||||
import usePermit2Approval from 'nft/hooks/usePermit2Approval'
|
||||
import { useTokenInput } from 'nft/hooks/useTokenInput'
|
||||
import { useWalletBalance } from 'nft/hooks/useWalletBalance'
|
||||
import { BagStatus } from 'nft/types'
|
||||
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils'
|
||||
import { PropsWithChildren, useMemo, useReducer } from 'react'
|
||||
import { PropsWithChildren, useMemo, useState } from 'react'
|
||||
import { AlertTriangle, ChevronDown } from 'react-feather'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { warningSeverity } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { BagTokenSelectorModal } from './tokenSelector/BagTokenSelectorModal'
|
||||
const LOW_SEVERITY_THRESHOLD = 1
|
||||
const MEDIUM_SEVERITY_THRESHOLD = 3
|
||||
|
||||
const FooterContainer = styled.div`
|
||||
padding: 0px 12px;
|
||||
@@ -41,14 +55,12 @@ const Footer = styled.div`
|
||||
border-bottom-right-radius: 12px;
|
||||
`
|
||||
|
||||
const FooterHeader = styled(Column)<{ warningText?: boolean }>`
|
||||
const FooterHeader = styled(Column)<{ usingPayWithAnyToken?: boolean }>`
|
||||
padding-top: 8px;
|
||||
padding-bottom: ${({ warningText }) => (warningText ? '8px' : '20px')};
|
||||
padding-bottom: ${({ usingPayWithAnyToken }) => (usingPayWithAnyToken ? '16px' : '20px')};
|
||||
`
|
||||
|
||||
const CurrencyRow = styled(Row)<{ warningText?: boolean }>`
|
||||
padding-top: 4px;
|
||||
padding-bottom: ${({ warningText }) => (warningText ? '8px' : '20px')};
|
||||
const CurrencyRow = styled(Row)`
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
`
|
||||
@@ -67,17 +79,26 @@ const WarningText = styled(ThemedText.BodyPrimary)`
|
||||
color: ${({ theme }) => theme.accentWarning};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 12px 0 !important;
|
||||
margin-bottom: 10px !important;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const HelperText = styled(ThemedText.Caption)<{ $color: string }>`
|
||||
color: ${({ $color }) => $color};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 10px !important;
|
||||
`
|
||||
|
||||
const CurrencyInput = styled(Row)`
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const PayButton = styled(Row)<{ disabled?: boolean }>`
|
||||
background: ${({ theme }) => theme.accentAction};
|
||||
const PayButton = styled.button<{ $backgroundColor: string }>`
|
||||
display: flex;
|
||||
background: ${({ $backgroundColor }) => $backgroundColor};
|
||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
@@ -87,18 +108,41 @@ const PayButton = styled(Row)<{ disabled?: boolean }>`
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 0px;
|
||||
opacity: ${({ disabled }) => (disabled ? 0.6 : 1)};
|
||||
cursor: ${({ disabled }) => (disabled ? 'auto' : 'pointer')};
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
const FiatLoadingBubble = styled(LoadingBubble)`
|
||||
border-radius: 4px;
|
||||
width: 4rem;
|
||||
height: 1rem;
|
||||
align-self: end;
|
||||
`
|
||||
const PriceImpactContainer = styled(Row)`
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const PriceImpactRow = styled(Row)`
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
interface ActionButtonProps {
|
||||
disabled?: boolean
|
||||
onClick: () => void
|
||||
backgroundColor: string
|
||||
}
|
||||
|
||||
const ActionButton = ({ disabled, children, onClick }: PropsWithChildren<ActionButtonProps>) => {
|
||||
const ActionButton = ({ disabled, children, onClick, backgroundColor }: PropsWithChildren<ActionButtonProps>) => {
|
||||
return (
|
||||
<PayButton disabled={disabled} onClick={onClick}>
|
||||
<PayButton disabled={disabled} onClick={onClick} $backgroundColor={backgroundColor}>
|
||||
{children}
|
||||
</PayButton>
|
||||
)
|
||||
@@ -116,9 +160,99 @@ const Warning = ({ children }: PropsWithChildren<unknown>) => {
|
||||
)
|
||||
}
|
||||
|
||||
interface HelperTextProps {
|
||||
color: string
|
||||
}
|
||||
|
||||
const Helper = ({ children, color }: PropsWithChildren<HelperTextProps>) => {
|
||||
if (!children) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<HelperText lineHeight="16px" $color={color}>
|
||||
{children}
|
||||
</HelperText>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: ask design about no route found
|
||||
const InputCurrencyValue = ({
|
||||
usingPayWithAnyToken,
|
||||
totalEthPrice,
|
||||
activeCurrency,
|
||||
tradeState,
|
||||
trade,
|
||||
}: {
|
||||
usingPayWithAnyToken: boolean
|
||||
totalEthPrice: BigNumber
|
||||
activeCurrency: Currency | undefined | null
|
||||
tradeState: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
}) => {
|
||||
if (!usingPayWithAnyToken) {
|
||||
return (
|
||||
<ThemedText.BodyPrimary lineHeight="20px" fontWeight="500">
|
||||
{formatWeiToDecimal(totalEthPrice.toString())}
|
||||
{activeCurrency?.symbol ?? 'ETH'}
|
||||
</ThemedText.BodyPrimary>
|
||||
)
|
||||
}
|
||||
|
||||
if (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING) {
|
||||
return (
|
||||
<ThemedText.BodyPrimary
|
||||
lineHeight="20px"
|
||||
fontWeight="500"
|
||||
color={tradeState === TradeState.VALID ? 'textPrimary' : 'textTertiary'}
|
||||
>
|
||||
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
|
||||
</ThemedText.BodyPrimary>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
|
||||
<Trans>Fetching price...</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
)
|
||||
}
|
||||
|
||||
const FiatValue = ({
|
||||
usdcValue,
|
||||
priceImpact,
|
||||
priceImpactColor,
|
||||
}: {
|
||||
usdcValue: CurrencyAmount<Token> | null
|
||||
priceImpact: Percent | undefined
|
||||
priceImpactColor: string | undefined
|
||||
}) => {
|
||||
if (!usdcValue) {
|
||||
return <FiatLoadingBubble />
|
||||
}
|
||||
|
||||
return (
|
||||
<PriceImpactContainer>
|
||||
{priceImpact && priceImpactColor && (
|
||||
<>
|
||||
<MouseoverTooltip text={t`The estimated difference between the USD values of input and output amounts.`}>
|
||||
<PriceImpactRow>
|
||||
<AlertTriangle color={priceImpactColor} size="16px" />
|
||||
<ThemedText.BodySmall style={{ color: priceImpactColor }} lineHeight="20px">
|
||||
(<Trans>{formatPriceImpact(priceImpact)}</Trans>)
|
||||
</ThemedText.BodySmall>
|
||||
</PriceImpactRow>
|
||||
</MouseoverTooltip>
|
||||
</>
|
||||
)}
|
||||
<ThemedText.BodySmall color="textTertiary" lineHeight="20px">
|
||||
{`${ethNumberStandardFormatter(usdcValue?.toExact(), true)}`}
|
||||
</ThemedText.BodySmall>
|
||||
</PriceImpactContainer>
|
||||
)
|
||||
}
|
||||
|
||||
interface BagFooterProps {
|
||||
totalEthPrice: BigNumber
|
||||
totalUsdPrice: number | undefined
|
||||
bagStatus: BagStatus
|
||||
fetchAssets: () => void
|
||||
eventProperties: Record<string, unknown>
|
||||
@@ -131,24 +265,18 @@ const PENDING_BAG_STATUSES = [
|
||||
BagStatus.PROCESSING_TRANSACTION,
|
||||
]
|
||||
|
||||
export const BagFooter = ({
|
||||
totalEthPrice,
|
||||
totalUsdPrice,
|
||||
bagStatus,
|
||||
fetchAssets,
|
||||
eventProperties,
|
||||
}: BagFooterProps) => {
|
||||
export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperties }: BagFooterProps) => {
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const theme = useTheme()
|
||||
const { account, chainId, connector } = useWeb3React()
|
||||
const connected = Boolean(account && chainId)
|
||||
const shouldUsePayWithAnyToken = usePayWithAnyTokenFlag() === PayWithAnyTokenVariant.Enabled
|
||||
const shouldUsePayWithAnyToken = usePayWithAnyTokenEnabled()
|
||||
const inputCurrency = useTokenInput((state) => state.inputCurrency)
|
||||
const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
|
||||
const defaultCurrency = useCurrency('ETH')
|
||||
|
||||
const setBagExpanded = useBag((state) => state.setBagExpanded)
|
||||
const [showTokenSelector, toggleTokenSelector] = useReducer((state) => !state, false)
|
||||
const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false)
|
||||
|
||||
const { balance: balanceInEth } = useWalletBalance()
|
||||
const sufficientBalance = useMemo(() => {
|
||||
@@ -158,11 +286,52 @@ export const BagFooter = ({
|
||||
return parseEther(balanceInEth).gte(totalEthPrice)
|
||||
}, [connected, chainId, balanceInEth, totalEthPrice])
|
||||
|
||||
const { buttonText, disabled, warningText, handleClick } = useMemo(() => {
|
||||
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
|
||||
const activeCurrency = inputCurrency ?? defaultCurrency
|
||||
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken
|
||||
|
||||
const parsedOutputAmount = useMemo(() => {
|
||||
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
|
||||
}, [defaultCurrency, totalEthPrice])
|
||||
const { state: tradeState, trade, maximumAmountIn } = usePayWithAnyTokenSwap(inputCurrency, parsedOutputAmount)
|
||||
const { allowance, isAllowancePending, isApprovalLoading, updateAllowance } = usePermit2Approval(
|
||||
trade?.inputAmount.currency.isToken ? (trade?.inputAmount as CurrencyAmount<Token>) : undefined,
|
||||
maximumAmountIn,
|
||||
shouldUsePayWithAnyToken
|
||||
)
|
||||
|
||||
const fiatValueTradeInput = useStablecoinValue(trade?.inputAmount)
|
||||
const fiatValueTradeOutput = useStablecoinValue(parsedOutputAmount)
|
||||
const usdcValue = usingPayWithAnyToken ? fiatValueTradeInput : fiatValueTradeOutput
|
||||
const stablecoinPriceImpact = useMemo(
|
||||
() =>
|
||||
tradeState === TradeState.SYNCING || !trade
|
||||
? undefined
|
||||
: computeFiatValuePriceImpact(fiatValueTradeInput, fiatValueTradeOutput),
|
||||
[fiatValueTradeInput, fiatValueTradeOutput, tradeState, trade]
|
||||
)
|
||||
const { priceImpactWarning, priceImpactColor } = useMemo(() => {
|
||||
const severity = warningSeverity(stablecoinPriceImpact)
|
||||
|
||||
if (severity < LOW_SEVERITY_THRESHOLD) {
|
||||
return { priceImpactWarning: false, priceImpactColor: undefined }
|
||||
}
|
||||
|
||||
if (severity < MEDIUM_SEVERITY_THRESHOLD) {
|
||||
return { priceImpactWarning: false, priceImpactColor: theme.accentWarning }
|
||||
}
|
||||
|
||||
return { priceImpactWarning: true, priceImpactColor: theme.accentCritical }
|
||||
}, [stablecoinPriceImpact, theme.accentCritical, theme.accentWarning])
|
||||
|
||||
const { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor } = useMemo(() => {
|
||||
let handleClick = fetchAssets
|
||||
let buttonText = <Trans>Something went wrong</Trans>
|
||||
let disabled = true
|
||||
let warningText = null
|
||||
let warningText = undefined
|
||||
let helperText = undefined
|
||||
let helperTextColor = theme.textSecondary
|
||||
let buttonColor = theme.accentAction
|
||||
|
||||
if (connected && chainId !== SupportedChainId.MAINNET) {
|
||||
handleClick = () => switchChain(connector, SupportedChainId.MAINNET)
|
||||
@@ -182,98 +351,139 @@ export const BagFooter = ({
|
||||
}
|
||||
disabled = false
|
||||
buttonText = <Trans>Connect wallet</Trans>
|
||||
} else if (usingPayWithAnyToken && tradeState !== TradeState.VALID) {
|
||||
disabled = true
|
||||
buttonText = <Trans>Fetching Route</Trans>
|
||||
} else if (allowance.state === AllowanceState.REQUIRED || allowance.state === AllowanceState.LOADING) {
|
||||
handleClick = () => updateAllowance()
|
||||
disabled = isAllowancePending || isApprovalLoading || allowance.state === AllowanceState.LOADING
|
||||
|
||||
if (allowance.state === AllowanceState.LOADING) {
|
||||
buttonText = <Trans>Loading Allowance</Trans>
|
||||
} else if (isAllowancePending) {
|
||||
buttonText = <Trans>Approve in your wallet</Trans>
|
||||
} else if (isApprovalLoading) {
|
||||
buttonText = <Trans>Approval pending</Trans>
|
||||
} else {
|
||||
helperText = <Trans>An approval is needed to use this token. </Trans>
|
||||
buttonText = <Trans>Approve</Trans>
|
||||
}
|
||||
} else if (bagStatus === BagStatus.FETCHING_FINAL_ROUTE || bagStatus === BagStatus.CONFIRMING_IN_WALLET) {
|
||||
disabled = true
|
||||
buttonText = <Trans>Proceed in wallet</Trans>
|
||||
} else if (bagStatus === BagStatus.PROCESSING_TRANSACTION) {
|
||||
disabled = true
|
||||
buttonText = <Trans>Transaction pending</Trans>
|
||||
} else if (priceImpactWarning && priceImpactColor) {
|
||||
disabled = false
|
||||
buttonColor = priceImpactColor
|
||||
helperText = <Trans>Price impact warning</Trans>
|
||||
helperTextColor = priceImpactColor
|
||||
buttonText = <Trans>Pay Anyway</Trans>
|
||||
} else if (sufficientBalance === true) {
|
||||
disabled = false
|
||||
buttonText = <Trans>Pay</Trans>
|
||||
}
|
||||
|
||||
return { buttonText, disabled, warningText, handleClick }
|
||||
}, [bagStatus, chainId, connected, connector, fetchAssets, setBagExpanded, sufficientBalance, toggleWalletModal])
|
||||
return { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor }
|
||||
}, [
|
||||
fetchAssets,
|
||||
theme.textSecondary,
|
||||
theme.accentAction,
|
||||
connected,
|
||||
chainId,
|
||||
sufficientBalance,
|
||||
bagStatus,
|
||||
usingPayWithAnyToken,
|
||||
tradeState,
|
||||
allowance.state,
|
||||
priceImpactWarning,
|
||||
priceImpactColor,
|
||||
connector,
|
||||
toggleWalletModal,
|
||||
setBagExpanded,
|
||||
isAllowancePending,
|
||||
isApprovalLoading,
|
||||
updateAllowance,
|
||||
])
|
||||
|
||||
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
|
||||
const activeCurrency = inputCurrency ?? defaultCurrency
|
||||
const traceEventProperties = {
|
||||
usd_value: usdcValue?.toExact(),
|
||||
...eventProperties,
|
||||
}
|
||||
|
||||
return (
|
||||
<FooterContainer>
|
||||
<Footer>
|
||||
{shouldUsePayWithAnyToken && (
|
||||
<CurrencyRow>
|
||||
<Column gap="xs">
|
||||
<ThemedText.SubHeaderSmall>
|
||||
<Trans>Pay with</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<CurrencyInput onClick={toggleTokenSelector}>
|
||||
<CurrencyLogo currency={activeCurrency} size="24px" />
|
||||
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
|
||||
{activeCurrency?.symbol}
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ChevronDown size={20} color={theme.textSecondary} />
|
||||
</CurrencyInput>
|
||||
</Column>
|
||||
<TotalColumn gap="xs">
|
||||
<ThemedText.SubHeaderSmall marginBottom="4px">
|
||||
<Trans>Total</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ThemedText.HeadlineSmall>
|
||||
{formatWeiToDecimal(totalEthPrice.toString())} {activeCurrency?.symbol ?? 'ETH'}
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">{`${ethNumberStandardFormatter(
|
||||
totalUsdPrice,
|
||||
true
|
||||
)}`}</ThemedText.BodySmall>
|
||||
</TotalColumn>
|
||||
</CurrencyRow>
|
||||
<FooterHeader gap="xs" usingPayWithAnyToken={shouldUsePayWithAnyToken}>
|
||||
<CurrencyRow>
|
||||
<Column gap="xs">
|
||||
<ThemedText.SubHeaderSmall>
|
||||
<Trans>Pay with</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<CurrencyInput onClick={() => setTokenSelectorOpen(true)}>
|
||||
<CurrencyLogo currency={activeCurrency} size="24px" />
|
||||
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
|
||||
{activeCurrency?.symbol}
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ChevronDown size={20} color={theme.textSecondary} />
|
||||
</CurrencyInput>
|
||||
</Column>
|
||||
<TotalColumn gap="xs">
|
||||
<ThemedText.SubHeaderSmall marginBottom="4px">
|
||||
<Trans>Total</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<InputCurrencyValue
|
||||
usingPayWithAnyToken={usingPayWithAnyToken}
|
||||
totalEthPrice={totalEthPrice}
|
||||
activeCurrency={activeCurrency}
|
||||
tradeState={tradeState}
|
||||
trade={trade}
|
||||
/>
|
||||
</TotalColumn>
|
||||
</CurrencyRow>
|
||||
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} />
|
||||
</FooterHeader>
|
||||
)}
|
||||
{!shouldUsePayWithAnyToken && (
|
||||
<FooterHeader gap="xs" warningText={!!warningText}>
|
||||
<FooterHeader gap="xs">
|
||||
<Row justify="space-between">
|
||||
<div>
|
||||
<ThemedText.HeadlineSmall>Total</ThemedText.HeadlineSmall>
|
||||
</div>
|
||||
<div>
|
||||
<ThemedText.HeadlineSmall>
|
||||
{formatWeiToDecimal(totalEthPrice.toString())} ETH
|
||||
{formatWeiToDecimal(totalEthPrice.toString())}
|
||||
{activeCurrency?.symbol ?? 'ETH'}
|
||||
</ThemedText.HeadlineSmall>
|
||||
</div>
|
||||
</Row>
|
||||
<Row justify="flex-end">
|
||||
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">{`${ethNumberStandardFormatter(
|
||||
totalUsdPrice,
|
||||
true
|
||||
)}`}</ThemedText.BodySmall>
|
||||
</Row>
|
||||
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} />
|
||||
</FooterHeader>
|
||||
)}
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={NFTEventName.NFT_BUY_BAG_PAY}
|
||||
element={InterfaceElementName.NFT_BUY_BAG_PAY_BUTTON}
|
||||
properties={{ ...eventProperties }}
|
||||
properties={{ ...traceEventProperties }}
|
||||
shouldLogImpression={connected && !disabled}
|
||||
>
|
||||
<Warning>{warningText}</Warning>
|
||||
<ActionButton onClick={handleClick} disabled={disabled}>
|
||||
<Helper color={helperTextColor}>{helperText}</Helper>
|
||||
<ActionButton onClick={handleClick} disabled={disabled} backgroundColor={buttonColor}>
|
||||
{isPending && <Loader size="20px" stroke="white" />}
|
||||
{buttonText}
|
||||
</ActionButton>
|
||||
</TraceEvent>
|
||||
</Footer>
|
||||
{showTokenSelector && (
|
||||
<BagTokenSelectorModal
|
||||
selectedCurrency={activeCurrency ?? undefined}
|
||||
handleCurrencySelect={(currency: Currency) => {
|
||||
setInputCurrency(currency)
|
||||
toggleTokenSelector()
|
||||
}}
|
||||
overlayClick={toggleTokenSelector}
|
||||
/>
|
||||
)}
|
||||
<CurrencySearchModal
|
||||
isOpen={tokenSelectorOpen}
|
||||
onDismiss={() => setTokenSelectorOpen(false)}
|
||||
onCurrencySelect={(currency: Currency) => setInputCurrency(currency.isNative ? undefined : currency)}
|
||||
selectedCurrency={activeCurrency ?? undefined}
|
||||
onlyShowCurrenciesWithBalance={true}
|
||||
/>
|
||||
</FooterContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { Plural, t } from '@lingui/macro'
|
||||
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import ms from 'ms.macro'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { ArrowRightIcon, HazardIcon, LoadingIcon, XMarkIcon } from 'nft/components/icons'
|
||||
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
|
||||
import { bodySmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useNFTList, useSellAsset } from 'nft/hooks'
|
||||
import { Listing, ListingStatus, WalletAsset } from 'nft/types'
|
||||
import { pluralize } from 'nft/utils/roundAndPluralize'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import * as styles from './ListingModal.css'
|
||||
import { getListings } from './utils'
|
||||
|
||||
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
|
||||
|
||||
interface ListingButtonProps {
|
||||
onClick: () => void
|
||||
buttonText: string
|
||||
@@ -20,18 +27,46 @@ interface ListingButtonProps {
|
||||
}
|
||||
|
||||
export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => {
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const addMarketplaceWarning = useSellAsset((state) => state.addMarketplaceWarning)
|
||||
const removeAllMarketplaceWarnings = useSellAsset((state) => state.removeAllMarketplaceWarnings)
|
||||
const listingStatus = useNFTList((state) => state.listingStatus)
|
||||
const setListingStatus = useNFTList((state) => state.setListingStatus)
|
||||
const setListings = useNFTList((state) => state.setListings)
|
||||
const setCollectionsRequiringApproval = useNFTList((state) => state.setCollectionsRequiringApproval)
|
||||
const {
|
||||
addMarketplaceWarning,
|
||||
sellAssets,
|
||||
removeAllMarketplaceWarnings,
|
||||
showResolveIssues,
|
||||
toggleShowResolveIssues,
|
||||
} = useSellAsset(
|
||||
({
|
||||
addMarketplaceWarning,
|
||||
sellAssets,
|
||||
removeAllMarketplaceWarnings,
|
||||
showResolveIssues,
|
||||
toggleShowResolveIssues,
|
||||
}) => ({
|
||||
addMarketplaceWarning,
|
||||
sellAssets,
|
||||
removeAllMarketplaceWarnings,
|
||||
showResolveIssues,
|
||||
toggleShowResolveIssues,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const { listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval } = useNFTList(
|
||||
({ listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval }) => ({
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
|
||||
const [showWarning, setShowWarning] = useState(false)
|
||||
const [canContinue, setCanContinue] = useState(false)
|
||||
const [issues, setIssues] = useState(0)
|
||||
const theme = useTheme()
|
||||
const warningRef = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(warningRef, () => {
|
||||
setShowWarning(false)
|
||||
!isNftListV2 && setShowWarning(false)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,13 +106,30 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
|
||||
for (const listing of asset.newListings) {
|
||||
if (!listing.price) listingsMissingPrice.push([asset, listing])
|
||||
else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
|
||||
else if (listing.price < (asset?.floorPrice ?? 0) && !listing.overrideFloorPrice)
|
||||
else if (
|
||||
listing.price < (asset?.floorPrice ?? 0) * BELOW_FLOOR_PRICE_THRESHOLD &&
|
||||
!listing.overrideFloorPrice
|
||||
)
|
||||
listingsBelowFloor.push([asset, listing])
|
||||
else if (asset.floor_sell_order_price && listing.price > asset.floor_sell_order_price)
|
||||
else if (asset.floor_sell_order_price && listing.price >= asset.floor_sell_order_price)
|
||||
listingsAboveSellOrderFloor.push([asset, listing])
|
||||
}
|
||||
}
|
||||
}
|
||||
// set number of issues
|
||||
if (isNftListV2) {
|
||||
const foundIssues =
|
||||
Number(missingExpiration) +
|
||||
Number(overMaxExpiration) +
|
||||
listingsMissingPrice.length +
|
||||
listingsAboveSellOrderFloor.length
|
||||
setIssues(foundIssues)
|
||||
!foundIssues && showResolveIssues && toggleShowResolveIssues()
|
||||
// Only show Resolve Issue text if there was a user submitted error (ie not when page loads with no prices set)
|
||||
if ((missingExpiration || overMaxExpiration || listingsAboveSellOrderFloor.length) && !showResolveIssues)
|
||||
toggleShowResolveIssues()
|
||||
}
|
||||
|
||||
const continueCheck = listingsBelowFloor.length === 0 && listingsAboveSellOrderFloor.length === 0
|
||||
setCanContinue(continueCheck)
|
||||
return [
|
||||
@@ -90,7 +142,7 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
|
||||
listingsAboveSellOrderFloor,
|
||||
invalidPrices,
|
||||
]
|
||||
}, [sellAssets])
|
||||
}, [isNftListV2, sellAssets, showResolveIssues, toggleShowResolveIssues])
|
||||
|
||||
const [disableListButton, warningMessage] = useMemo(() => {
|
||||
const disableListButton =
|
||||
@@ -158,94 +210,116 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
|
||||
}
|
||||
|
||||
const warningWrappedClick = () => {
|
||||
if ((!disableListButton && canContinue) || showWarningOverride) onClick()
|
||||
else addWarningMessages()
|
||||
if ((!disableListButton && canContinue) || showWarningOverride) {
|
||||
if (issues && isNftListV2) !showResolveIssues && toggleShowResolveIssues()
|
||||
else if (listingsBelowFloor.length) setShowWarning(true)
|
||||
else onClick()
|
||||
} else addWarningMessages()
|
||||
}
|
||||
|
||||
return (
|
||||
<Box position="relative" width="full">
|
||||
{!showWarningOverride && showWarning && warningMessage.length > 0 && (
|
||||
<Row
|
||||
className={`${bodySmall} ${styles.warningTooltip}`}
|
||||
transition="250"
|
||||
onClick={() => setShowWarning(false)}
|
||||
color="textSecondary"
|
||||
zIndex="3"
|
||||
borderRadius="4"
|
||||
backgroundColor="backgroundSurface"
|
||||
height={!disableListButton ? '64' : '36'}
|
||||
maxWidth="276"
|
||||
position="absolute"
|
||||
left="24"
|
||||
bottom="52"
|
||||
flexWrap={!disableListButton ? 'wrap' : 'nowrap'}
|
||||
style={{ maxWidth: !disableListButton ? '225px' : '' }}
|
||||
ref={warningRef}
|
||||
>
|
||||
<HazardIcon />
|
||||
<Box marginLeft="4" marginRight="8">
|
||||
{warningMessage}
|
||||
</Box>
|
||||
{disableListButton ? (
|
||||
<Box paddingTop="6">
|
||||
<XMarkIcon fill={themeVars.colors.textSecondary} height="20" width="20" />
|
||||
<>
|
||||
<Box position="relative" width="full">
|
||||
{!showWarningOverride && showWarning && warningMessage.length > 0 && (
|
||||
<Row
|
||||
className={`${bodySmall} ${styles.warningTooltip}`}
|
||||
transition="250"
|
||||
onClick={() => setShowWarning(false)}
|
||||
color="textSecondary"
|
||||
zIndex="3"
|
||||
borderRadius="4"
|
||||
backgroundColor="backgroundSurface"
|
||||
height={!disableListButton ? '64' : '36'}
|
||||
maxWidth="276"
|
||||
position="absolute"
|
||||
left="24"
|
||||
bottom="52"
|
||||
flexWrap={!disableListButton ? 'wrap' : 'nowrap'}
|
||||
style={{ maxWidth: !disableListButton ? '225px' : '' }}
|
||||
ref={warningRef}
|
||||
>
|
||||
<HazardIcon />
|
||||
<Box marginLeft="4" marginRight="8">
|
||||
{warningMessage}
|
||||
</Box>
|
||||
) : (
|
||||
<Row
|
||||
marginLeft="72"
|
||||
cursor="pointer"
|
||||
color="accentAction"
|
||||
onClick={() => {
|
||||
setShowWarning(false)
|
||||
setCanContinue(true)
|
||||
onClick()
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
<ArrowRightIcon height="20" width="20" />
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
<Box
|
||||
as="button"
|
||||
border="none"
|
||||
backgroundColor="accentAction"
|
||||
cursor={
|
||||
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
|
||||
disableListButton
|
||||
? 'default'
|
||||
: 'pointer'
|
||||
}
|
||||
color="explicitWhite"
|
||||
className={styles.button}
|
||||
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
|
||||
type="button"
|
||||
style={{
|
||||
opacity:
|
||||
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
|
||||
disableListButton
|
||||
? 0.3
|
||||
: 1,
|
||||
}}
|
||||
>
|
||||
{listingStatus === ListingStatus.SIGNING || listingStatus === ListingStatus.PENDING ? (
|
||||
<Row gap="8">
|
||||
<LoadingIcon stroke="backgroundSurface" height="20" width="20" />
|
||||
{listingStatus === ListingStatus.PENDING ? 'Pending' : 'Proceed in wallet'}
|
||||
{disableListButton ? (
|
||||
<Box paddingTop="6">
|
||||
<XMarkIcon fill={themeVars.colors.textSecondary} height="20" width="20" />
|
||||
</Box>
|
||||
) : (
|
||||
<Row
|
||||
marginLeft="72"
|
||||
cursor="pointer"
|
||||
color="accentAction"
|
||||
onClick={() => {
|
||||
setShowWarning(false)
|
||||
setCanContinue(true)
|
||||
onClick()
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
<ArrowRightIcon height="20" width="20" />
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
) : listingStatus === ListingStatus.APPROVED ? (
|
||||
'Complete!'
|
||||
) : listingStatus === ListingStatus.PAUSED ? (
|
||||
'Paused'
|
||||
) : listingStatus === ListingStatus.FAILED ? (
|
||||
'Try again'
|
||||
) : listingStatus === ListingStatus.CONTINUE ? (
|
||||
'Continue'
|
||||
) : (
|
||||
buttonText
|
||||
)}
|
||||
<Box
|
||||
as="button"
|
||||
border="none"
|
||||
backgroundColor={showResolveIssues ? 'accentFailure' : 'accentAction'}
|
||||
cursor={
|
||||
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
|
||||
disableListButton
|
||||
? 'default'
|
||||
: 'pointer'
|
||||
}
|
||||
className={styles.button}
|
||||
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
|
||||
type="button"
|
||||
style={{
|
||||
color: showResolveIssues ? theme.accentTextDarkPrimary : theme.white,
|
||||
opacity:
|
||||
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
|
||||
(disableListButton && !showResolveIssues)
|
||||
? 0.3
|
||||
: 1,
|
||||
}}
|
||||
>
|
||||
{listingStatus === ListingStatus.SIGNING || listingStatus === ListingStatus.PENDING ? (
|
||||
isNftListV2 ? (
|
||||
listingStatus === ListingStatus.PENDING ? (
|
||||
'Pending'
|
||||
) : (
|
||||
'Proceed in wallet'
|
||||
)
|
||||
) : (
|
||||
<Row gap="8">
|
||||
<LoadingIcon stroke="backgroundSurface" height="20" width="20" />
|
||||
{listingStatus === ListingStatus.PENDING ? 'Pending' : 'Proceed in wallet'}
|
||||
</Row>
|
||||
)
|
||||
) : listingStatus === ListingStatus.APPROVED ? (
|
||||
'Complete!'
|
||||
) : listingStatus === ListingStatus.PAUSED ? (
|
||||
'Paused'
|
||||
) : listingStatus === ListingStatus.FAILED ? (
|
||||
'Try again'
|
||||
) : listingStatus === ListingStatus.CONTINUE ? (
|
||||
'Continue'
|
||||
) : showResolveIssues ? (
|
||||
<Plural value={issues !== 1 ? 2 : 1} _1="Resolve issue" other={t`Resolve ${issues} issues`} />
|
||||
) : (
|
||||
buttonText
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{showWarning && (
|
||||
<BelowFloorWarningModal
|
||||
listingsBelowFloor={listingsBelowFloor}
|
||||
closeModal={() => setShowWarning(false)}
|
||||
startListing={onClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
|
||||
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
@@ -13,6 +12,7 @@ import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
|
||||
import { fetchPrice } from 'nft/utils/fetchPrice'
|
||||
import { pluralize } from 'nft/utils/roundAndPluralize'
|
||||
import { Dispatch, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import { ListingButton } from './ListingButton'
|
||||
import * as styles from './ListingModal.css'
|
||||
@@ -22,18 +22,49 @@ import { approveCollectionRow, getTotalEthValue, pauseRow, resetRow, signListing
|
||||
const ListingModal = () => {
|
||||
const { provider } = useWeb3React()
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const {
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
setListingStatusAndCallback,
|
||||
setCollectionStatusAndCallback,
|
||||
looksRareNonce,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
} = useNFTList(
|
||||
({
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
setListingStatusAndCallback,
|
||||
setCollectionStatusAndCallback,
|
||||
looksRareNonce,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}) => ({
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
setListingStatusAndCallback,
|
||||
setCollectionStatusAndCallback,
|
||||
looksRareNonce,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const signer = provider?.getSigner()
|
||||
const listings = useNFTList((state) => state.listings)
|
||||
const setListings = useNFTList((state) => state.setListings)
|
||||
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
|
||||
const setCollectionsRequiringApproval = useNFTList((state) => state.setCollectionsRequiringApproval)
|
||||
const [openIndex, setOpenIndex] = useState(0)
|
||||
const listingStatus = useNFTList((state) => state.listingStatus)
|
||||
const setListingStatus = useNFTList((state) => state.setListingStatus)
|
||||
const [allCollectionsApproved, setAllCollectionsApproved] = useState(false)
|
||||
const looksRareNonce = useNFTList((state) => state.looksRareNonce)
|
||||
const setLooksRareNonce = useNFTList((state) => state.setLooksRareNonce)
|
||||
const getLooksRareNonce = useNFTList((state) => state.getLooksRareNonce)
|
||||
const toggleCart = useBag((state) => state.toggleBag)
|
||||
const looksRareNonceRef = useRef(looksRareNonce)
|
||||
const isMobile = useIsMobile()
|
||||
@@ -109,7 +140,6 @@ const ListingModal = () => {
|
||||
if (!signer) return
|
||||
sendAnalyticsEvent(NFTEventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
|
||||
setListingStatus(ListingStatus.SIGNING)
|
||||
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
|
||||
const signerAddress = await signer.getAddress()
|
||||
const nonce = await looksRareNonceFetcher(signerAddress)
|
||||
setLooksRareNonce(nonce ?? 0)
|
||||
@@ -118,27 +148,12 @@ const ListingModal = () => {
|
||||
setListingStatus(ListingStatus.SIGNING)
|
||||
setOpenIndex(1)
|
||||
}
|
||||
const looksRareAddress = addresses.TRANSFER_MANAGER_ERC721
|
||||
// for all unique collection, marketplace combos -> approve collections
|
||||
for (const collectionRow of collectionsRequiringApproval) {
|
||||
verifyStatus(collectionRow.status) &&
|
||||
(isMobile
|
||||
? await approveCollectionRow(
|
||||
collectionRow,
|
||||
collectionsRequiringApproval,
|
||||
setCollectionsRequiringApproval,
|
||||
signer,
|
||||
looksRareAddress,
|
||||
pauseAllRows
|
||||
)
|
||||
: approveCollectionRow(
|
||||
collectionRow,
|
||||
collectionsRequiringApproval,
|
||||
setCollectionsRequiringApproval,
|
||||
signer,
|
||||
looksRareAddress,
|
||||
pauseAllRows
|
||||
))
|
||||
? await approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows)
|
||||
: approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,12 +166,11 @@ const ListingModal = () => {
|
||||
verifyStatus(listing.status) &&
|
||||
(await signListingRow(
|
||||
listing,
|
||||
listings,
|
||||
setListings,
|
||||
signer,
|
||||
provider,
|
||||
getLooksRareNonce,
|
||||
setLooksRareNonce,
|
||||
setListingStatusAndCallback,
|
||||
pauseAllRows
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
|
||||
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
|
||||
import { LOOKSRARE_MARKETPLACE_CONTRACT, X2Y2_TRANSFER_CONTRACT } from 'nft/queries'
|
||||
import { OPENSEA_CROSS_CHAIN_CONDUIT } from 'nft/queries/openSea'
|
||||
import { AssetRow, CollectionRow, ListingMarket, ListingRow, ListingStatus, WalletAsset } from 'nft/types'
|
||||
@@ -28,28 +29,18 @@ const updateStatus = ({
|
||||
|
||||
export async function approveCollectionRow(
|
||||
collectionRow: CollectionRow,
|
||||
collectionsRequiringApproval: CollectionRow[],
|
||||
setCollectionsRequiringApproval: Dispatch<CollectionRow[]>,
|
||||
signer: JsonRpcSigner,
|
||||
looksRareAddress: string,
|
||||
pauseAllRows: () => void
|
||||
setCollectionStatusAndCallback: (
|
||||
collection: CollectionRow,
|
||||
status: ListingStatus,
|
||||
callback?: () => Promise<void>
|
||||
) => void,
|
||||
pauseAllRows?: () => void
|
||||
) {
|
||||
updateStatus({
|
||||
listing: collectionRow,
|
||||
newStatus: ListingStatus.SIGNING,
|
||||
rows: collectionsRequiringApproval,
|
||||
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
|
||||
callback: () =>
|
||||
approveCollectionRow(
|
||||
collectionRow,
|
||||
collectionsRequiringApproval,
|
||||
setCollectionsRequiringApproval,
|
||||
signer,
|
||||
looksRareAddress,
|
||||
pauseAllRows
|
||||
),
|
||||
})
|
||||
const callback = () => approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows)
|
||||
setCollectionStatusAndCallback(collectionRow, ListingStatus.SIGNING, callback)
|
||||
const { marketplace, collectionAddress } = collectionRow
|
||||
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
|
||||
const spender =
|
||||
marketplace.name === 'OpenSea'
|
||||
? OPENSEA_CROSS_CHAIN_CONDUIT
|
||||
@@ -57,67 +48,48 @@ export async function approveCollectionRow(
|
||||
? LOOKSRARE_MARKETPLACE_CONTRACT
|
||||
: marketplace.name === 'X2Y2'
|
||||
? X2Y2_TRANSFER_CONTRACT
|
||||
: looksRareAddress
|
||||
: addresses.TRANSFER_MANAGER_ERC721
|
||||
!!collectionAddress &&
|
||||
(await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
|
||||
updateStatus({
|
||||
listing: collectionRow,
|
||||
newStatus,
|
||||
rows: collectionsRequiringApproval,
|
||||
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
|
||||
})
|
||||
setCollectionStatusAndCallback(collectionRow, newStatus, callback)
|
||||
))
|
||||
if (collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) pauseAllRows()
|
||||
if (
|
||||
(collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) &&
|
||||
pauseAllRows
|
||||
)
|
||||
pauseAllRows()
|
||||
}
|
||||
|
||||
export async function signListingRow(
|
||||
listing: ListingRow,
|
||||
listings: ListingRow[],
|
||||
setListings: Dispatch<ListingRow[]>,
|
||||
signer: JsonRpcSigner,
|
||||
provider: Web3Provider,
|
||||
getLooksRareNonce: () => number,
|
||||
setLooksRareNonce: (nonce: number) => void,
|
||||
pauseAllRows: () => void
|
||||
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void,
|
||||
pauseAllRows?: () => void
|
||||
) {
|
||||
const looksRareNonce = getLooksRareNonce()
|
||||
updateStatus({
|
||||
listing,
|
||||
newStatus: ListingStatus.SIGNING,
|
||||
rows: listings,
|
||||
setRows: setListings as Dispatch<AssetRow[]>,
|
||||
callback: () => {
|
||||
return signListingRow(
|
||||
listing,
|
||||
listings,
|
||||
setListings,
|
||||
signer,
|
||||
provider,
|
||||
getLooksRareNonce,
|
||||
setLooksRareNonce,
|
||||
pauseAllRows
|
||||
)
|
||||
},
|
||||
})
|
||||
const callback = () => {
|
||||
return signListingRow(
|
||||
listing,
|
||||
signer,
|
||||
provider,
|
||||
getLooksRareNonce,
|
||||
setLooksRareNonce,
|
||||
setListingStatusAndCallback,
|
||||
pauseAllRows
|
||||
)
|
||||
}
|
||||
setListingStatusAndCallback(listing, ListingStatus.SIGNING, callback)
|
||||
const { asset, marketplace } = listing
|
||||
const res = await signListing(marketplace, asset, signer, provider, looksRareNonce, (newStatus: ListingStatus) =>
|
||||
updateStatus({
|
||||
listing,
|
||||
newStatus,
|
||||
rows: listings,
|
||||
setRows: setListings as Dispatch<AssetRow[]>,
|
||||
})
|
||||
setListingStatusAndCallback(listing, newStatus, callback)
|
||||
)
|
||||
if (listing.status === ListingStatus.REJECTED) pauseAllRows()
|
||||
else {
|
||||
if (listing.status === ListingStatus.REJECTED && pauseAllRows) {
|
||||
pauseAllRows()
|
||||
} else {
|
||||
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
|
||||
const newStatus = res ? ListingStatus.APPROVED : ListingStatus.FAILED
|
||||
updateStatus({
|
||||
listing,
|
||||
newStatus,
|
||||
rows: listings,
|
||||
setRows: setListings as Dispatch<AssetRow[]>,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +135,7 @@ export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], Listin
|
||||
name: asset.asset_contract.name,
|
||||
status: ListingStatus.DEFINED,
|
||||
collectionAddress: asset.asset_contract.address,
|
||||
isVerified: asset.collectionIsVerified,
|
||||
marketplace,
|
||||
}
|
||||
newCollectionsToApprove.push(newCollectionRow)
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { useAllTokens } from 'hooks/Tokens'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { tokenComparator } from 'lib/hooks/useTokenList/sorting'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import { useMemo } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useAllTokenBalances } from 'state/connection/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { CurrencyRow } from './CurrencyRow'
|
||||
|
||||
const ModalWrapper = styled(Column)`
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 420px;
|
||||
height: 368px;
|
||||
z-index: ${Z_INDEX.modalOverTooltip};
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px;
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
`
|
||||
|
||||
const TitleRow = styled(Row)`
|
||||
padding: 20px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const TokenSelectorContainer = styled(Column)`
|
||||
border-top: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
gap: 8px;
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
`
|
||||
|
||||
interface BagTokenSelectorModalProps {
|
||||
selectedCurrency: Currency | undefined
|
||||
handleCurrencySelect: (currency: Currency) => void
|
||||
overlayClick: () => void
|
||||
}
|
||||
|
||||
export const BagTokenSelectorModal = ({
|
||||
selectedCurrency,
|
||||
handleCurrencySelect,
|
||||
overlayClick,
|
||||
}: BagTokenSelectorModalProps) => {
|
||||
const defaultTokens = useAllTokens()
|
||||
const [balances, balancesAreLoading] = useAllTokenBalances()
|
||||
const sortedTokens: Token[] = useMemo(
|
||||
() =>
|
||||
!balancesAreLoading
|
||||
? Object.values(defaultTokens)
|
||||
.filter((token) => {
|
||||
return balances[token.address]?.greaterThan(0)
|
||||
})
|
||||
.sort(tokenComparator.bind(null, balances))
|
||||
: [],
|
||||
[balances, balancesAreLoading, defaultTokens]
|
||||
)
|
||||
|
||||
const native = useNativeCurrency()
|
||||
const wrapped = native.wrapped
|
||||
|
||||
const currencies: Currency[] = useMemo(() => {
|
||||
const tokens = sortedTokens.filter((t) => !t.equals(wrapped))
|
||||
const natives: Currency[] = []
|
||||
if (native.equals(wrapped)) {
|
||||
natives.push(wrapped)
|
||||
} else {
|
||||
natives.push(native)
|
||||
if (balances[wrapped.address]?.greaterThan(0)) {
|
||||
natives.push(wrapped)
|
||||
}
|
||||
}
|
||||
return [...natives, ...tokens]
|
||||
}, [sortedTokens, native, wrapped, balances])
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<ModalWrapper>
|
||||
<TitleRow>
|
||||
<ThemedText.SubHeader fontWeight={500} lineHeight="24px">
|
||||
<Trans>Select a token</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<X size={24} cursor="pointer" onClick={overlayClick} />
|
||||
</TitleRow>
|
||||
<TokenSelectorContainer>
|
||||
{currencies.map((currency) => {
|
||||
return (
|
||||
<CurrencyRow
|
||||
key={currency.isToken ? currency.wrapped.address : currency.name}
|
||||
currency={currency}
|
||||
selected={
|
||||
(!selectedCurrency && currency.isNative) || (!!selectedCurrency && selectedCurrency.equals(currency))
|
||||
}
|
||||
selectCurrency={handleCurrencySelect}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TokenSelectorContainer>
|
||||
</ModalWrapper>
|
||||
<Overlay onClick={overlayClick} />
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row from 'components/Row'
|
||||
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
|
||||
import { Check } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const TokenRow = styled(Row)`
|
||||
padding: 8px 0px;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const TokenInfoRow = styled(Row)`
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const StyledBalanceText = styled(ThemedText.SubHeader)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const StyledCheck = styled(Check)`
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
interface CurrencyRowProps {
|
||||
currency: Currency
|
||||
selected: boolean
|
||||
selectCurrency: (currency: Currency) => void
|
||||
}
|
||||
|
||||
export const CurrencyRow = ({ currency, selected, selectCurrency }: CurrencyRowProps) => {
|
||||
const { account } = useWeb3React()
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
|
||||
return (
|
||||
<TokenRow onClick={() => selectCurrency(currency)}>
|
||||
<TokenInfoRow>
|
||||
<CurrencyLogo currency={currency} size="36px" />
|
||||
<Column>
|
||||
<ThemedText.SubHeader fontWeight={500} lineHeight="24px">
|
||||
{currency.name}
|
||||
</ThemedText.SubHeader>
|
||||
<ThemedText.BodySmall lineHeight="20px" color="textSecondary">
|
||||
{currency.symbol}
|
||||
</ThemedText.BodySmall>
|
||||
</Column>
|
||||
</TokenInfoRow>
|
||||
{balance && <Balance balance={balance} />}
|
||||
{selected && <StyledCheck size={20} />}
|
||||
</TokenRow>
|
||||
)
|
||||
}
|
||||
|
||||
const Balance = ({ balance }: { balance: CurrencyAmount<Currency> }) => {
|
||||
return (
|
||||
<StyledBalanceText fontWeight={500} lineHeight="24px">
|
||||
{balance.toSignificant(4)}
|
||||
</StyledBalanceText>
|
||||
)
|
||||
}
|
||||
@@ -24,12 +24,12 @@ export const ChevronUpIcon = ({
|
||||
...props
|
||||
}: SVGProps & { secondaryWidth?: string; secondaryHeight?: string; secondaryColor?: string }) => (
|
||||
<svg
|
||||
{...props}
|
||||
width={secondaryWidth || '29'}
|
||||
height={secondaryHeight || '28'}
|
||||
viewBox="0 0 29 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_564_11230)">
|
||||
<path
|
||||
@@ -757,3 +757,16 @@ export const CancelListingIcon = (props: SVGProps) => (
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ListingModalWindowActive = (props: SVGProps) => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<circle cx="8" cy="8" r="8" fill={props.fill ? props.fill : themeVars.colors.accentAction} fillOpacity="0.24" />
|
||||
<circle cx="8" cy="8" r="5" fill={props.fill ? props.fill : themeVars.colors.accentAction} />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ListingModalWindowClosed = (props: SVGProps) => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<circle cx="8" cy="8" r="7" stroke="#333D59" strokeWidth="2" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
46
src/nft/components/profile/list/Dropdown.tsx
Normal file
46
src/nft/components/profile/list/Dropdown.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { DropDownOption } from 'nft/types'
|
||||
import { Check } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const DropdownWrapper = styled(Column)<{ $width: number }>`
|
||||
gap: 4px;
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
padding: 8px;
|
||||
width: ${({ $width }) => $width}px;
|
||||
border-radius: 12px;
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
|
||||
const DropdownRow = styled(Row)`
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.backgroundInteractive};
|
||||
}
|
||||
`
|
||||
|
||||
interface DropdownArgs {
|
||||
dropDownOptions: DropDownOption[]
|
||||
width: number
|
||||
}
|
||||
|
||||
export const Dropdown = ({ dropDownOptions, width }: DropdownArgs) => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<DropdownWrapper $width={width}>
|
||||
{dropDownOptions.map((option) => (
|
||||
<DropdownRow key={option.displayText} onClick={option.onClick}>
|
||||
<ThemedText.BodyPrimary lineHeight="24px">{option.displayText}</ThemedText.BodyPrimary>
|
||||
{option.isSelected && <Check height={20} width={20} color={theme.accentAction} />}
|
||||
</DropdownRow>
|
||||
))}
|
||||
</DropdownWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import ListingModal from 'nft/components/bag/profile/ListingModal'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
const ListModalWrapper = styled.div`
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 420px;
|
||||
z-index: ${Z_INDEX.modalOverTooltip};
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px;
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
padding: 0px 12px 4px;
|
||||
`
|
||||
|
||||
export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<ListModalWrapper>
|
||||
<ListingModal />
|
||||
</ListModalWrapper>
|
||||
<Overlay onClick={overlayClick} />
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { SMALL_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
|
||||
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||
import { ListingButton } from 'nft/components/bag/profile/ListingButton'
|
||||
import { getListingState, getTotalEthValue } from 'nft/components/bag/profile/utils'
|
||||
import { approveCollectionRow, getListingState, getTotalEthValue, verifyStatus } from 'nft/components/bag/profile/utils'
|
||||
import { BackArrowIcon } from 'nft/components/icons'
|
||||
import { headlineLarge, headlineSmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useBag, useIsMobile, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
|
||||
import { LIST_PAGE_MARGIN } from 'nft/pages/profile/shared'
|
||||
import { LIST_PAGE_MARGIN, LIST_PAGE_MARGIN_MOBILE, LIST_PAGE_MARGIN_TABLET } from 'nft/pages/profile/shared'
|
||||
import { looksRareNonceFetcher } from 'nft/queries'
|
||||
import { ListingStatus, ProfilePageStateType } from 'nft/types'
|
||||
import { fetchPrice, formatEth, formatUsdPrice } from 'nft/utils'
|
||||
import { ListingMarkets } from 'nft/utils/listNfts'
|
||||
import { useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import { ListModal } from './ListModal'
|
||||
import { ListModal } from './Modal/ListModal'
|
||||
import { NFTListingsGrid } from './NFTListingsGrid'
|
||||
import { SelectMarketplacesDropdown } from './SelectMarketplacesDropdown'
|
||||
import { SetDurationModal } from './SetDurationModal'
|
||||
@@ -68,10 +73,7 @@ const ListingHeader = styled(Row)`
|
||||
|
||||
const GridWrapper = styled.div`
|
||||
margin-top: 24px;
|
||||
|
||||
@media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
margin-left: 40px;
|
||||
}
|
||||
margin-bottom: 48px;
|
||||
`
|
||||
|
||||
const MobileListButtonWrapper = styled.div`
|
||||
@@ -98,6 +100,16 @@ const FloatingConfirmationBar = styled(Row)`
|
||||
transform: translateX(-50%);
|
||||
max-width: 1200px;
|
||||
z-index: ${Z_INDEX.under_dropdown};
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.lg}px) {
|
||||
width: calc(100% - ${LIST_PAGE_MARGIN_TABLET * 2}px);
|
||||
bottom: 68px;
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
width: calc(100% - ${LIST_PAGE_MARGIN_MOBILE * 2}px);
|
||||
}
|
||||
`
|
||||
|
||||
const Overlay = styled.div`
|
||||
@@ -108,9 +120,23 @@ const Overlay = styled.div`
|
||||
background: ${({ theme }) => `linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, ${theme.backgroundBackdrop} 100%)`};
|
||||
`
|
||||
|
||||
const UsdValue = styled(ThemedText.SubHeader)`
|
||||
line-height: 24px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
|
||||
const ProceedsAndButtonWrapper = styled(Row)`
|
||||
width: min-content;
|
||||
gap: 40px;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
gap: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const ProceedsWrapper = styled(Row)`
|
||||
@@ -118,27 +144,72 @@ const ProceedsWrapper = styled(Row)`
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const EthValueWrapper = styled.span<{ totalEthListingValue: boolean }>`
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: ${({ theme, totalEthListingValue }) => (totalEthListingValue ? theme.textPrimary : theme.textSecondary)};
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
`
|
||||
|
||||
const ListingButtonWrapper = styled.div`
|
||||
width: 170px;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
width: max-content;
|
||||
}
|
||||
`
|
||||
|
||||
export const ListPage = () => {
|
||||
const { setProfilePageState: setSellPageState } = useProfilePageState()
|
||||
const setGlobalMarketplaces = useSellAsset((state) => state.setGlobalMarketplaces)
|
||||
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
|
||||
const { provider } = useWeb3React()
|
||||
const toggleBag = useBag((s) => s.toggleBag)
|
||||
const listings = useNFTList((state) => state.listings)
|
||||
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
|
||||
const listingStatus = useNFTList((state) => state.listingStatus)
|
||||
const setListingStatus = useNFTList((state) => state.setListingStatus)
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const isMobile = useIsMobile()
|
||||
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
|
||||
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
|
||||
const { setGlobalMarketplaces, sellAssets } = useSellAsset(
|
||||
({ setGlobalMarketplaces, sellAssets }) => ({
|
||||
setGlobalMarketplaces,
|
||||
sellAssets,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const {
|
||||
listings,
|
||||
collectionsRequiringApproval,
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setLooksRareNonce,
|
||||
setCollectionStatusAndCallback,
|
||||
} = useNFTList(
|
||||
({
|
||||
listings,
|
||||
collectionsRequiringApproval,
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setLooksRareNonce,
|
||||
setCollectionStatusAndCallback,
|
||||
}) => ({
|
||||
listings,
|
||||
collectionsRequiringApproval,
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setLooksRareNonce,
|
||||
setCollectionStatusAndCallback,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
|
||||
const anyListingsMissingPrice = useMemo(() => !!listings.find((listing) => !listing.price), [listings])
|
||||
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
|
||||
const [showListModal, toggleShowListModal] = useReducer((s) => !s, false)
|
||||
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
|
||||
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
|
||||
const signer = provider?.getSigner()
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrice().then((price) => {
|
||||
@@ -146,6 +217,7 @@ export const ListPage = () => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// TODO with removal of list v1 see if this logic can be removed
|
||||
useEffect(() => {
|
||||
const state = getListingState(collectionsRequiringApproval, listings)
|
||||
|
||||
@@ -162,8 +234,48 @@ export const ListPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalMarketplaces(selectedMarkets)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedMarkets])
|
||||
}, [selectedMarkets, setGlobalMarketplaces])
|
||||
|
||||
const startListingEventProperties = {
|
||||
collection_addresses: sellAssets.map((asset) => asset.asset_contract.address),
|
||||
token_ids: sellAssets.map((asset) => asset.tokenId),
|
||||
marketplaces: Array.from(new Set(listings.map((asset) => asset.marketplace.name))),
|
||||
list_quantity: listings.length,
|
||||
usd_value: ethPriceInUSD * totalEthListingValue,
|
||||
...trace,
|
||||
}
|
||||
|
||||
const startListingFlow = async () => {
|
||||
if (!signer) return
|
||||
sendAnalyticsEvent(NFTEventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
|
||||
setListingStatus(ListingStatus.SIGNING)
|
||||
const signerAddress = await signer.getAddress()
|
||||
const nonce = await looksRareNonceFetcher(signerAddress)
|
||||
setLooksRareNonce(nonce ?? 0)
|
||||
|
||||
// for all unique collection, marketplace combos -> approve collections
|
||||
for (const collectionRow of collectionsRequiringApproval) {
|
||||
verifyStatus(collectionRow.status) &&
|
||||
(isMobile
|
||||
? await approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback)
|
||||
: approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback))
|
||||
}
|
||||
}
|
||||
|
||||
const handleV2Click = () => {
|
||||
toggleShowListModal()
|
||||
startListingFlow()
|
||||
}
|
||||
|
||||
const BannerText = isMobile ? (
|
||||
<ThemedText.SubHeader lineHeight="24px">
|
||||
<Trans>Proceeds</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
) : (
|
||||
<ThemedText.HeadlineSmall lineHeight="28px">
|
||||
<Trans>Proceeds if sold</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
)
|
||||
|
||||
return (
|
||||
<Column>
|
||||
@@ -191,27 +303,21 @@ export const ListPage = () => {
|
||||
{isNftListV2 && (
|
||||
<>
|
||||
<FloatingConfirmationBar>
|
||||
<ThemedText.HeadlineSmall lineHeight="28px">
|
||||
<Trans>Proceeds if sold</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
{BannerText}
|
||||
<ProceedsAndButtonWrapper>
|
||||
<ProceedsWrapper>
|
||||
<ThemedText.HeadlineSmall
|
||||
lineHeight="28px"
|
||||
color={totalEthListingValue ? 'textPrimary' : 'textTertiary'}
|
||||
>
|
||||
<EthValueWrapper totalEthListingValue={!!totalEthListingValue}>
|
||||
{totalEthListingValue > 0 ? formatEth(totalEthListingValue) : '-'} ETH
|
||||
</ThemedText.HeadlineSmall>
|
||||
</EthValueWrapper>
|
||||
{!!totalEthListingValue && !!ethPriceInUSD && (
|
||||
<ThemedText.HeadlineSmall lineHeight="28px" color="textSecondary">
|
||||
{formatUsdPrice(totalEthListingValue * ethPriceInUSD)}
|
||||
</ThemedText.HeadlineSmall>
|
||||
<UsdValue>{formatUsdPrice(totalEthListingValue * ethPriceInUSD)}</UsdValue>
|
||||
)}
|
||||
</ProceedsWrapper>
|
||||
<ListingButtonWrapper>
|
||||
<ListingButton
|
||||
onClick={isNftListV2 ? toggleShowListModal : toggleBag}
|
||||
buttonText={anyListingsMissingPrice ? t`Set prices to continue` : t`Start listing`}
|
||||
onClick={handleV2Click}
|
||||
buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`}
|
||||
showWarningOverride={true}
|
||||
/>
|
||||
</ListingButtonWrapper>
|
||||
</ProceedsAndButtonWrapper>
|
||||
|
||||
@@ -3,12 +3,13 @@ import { t } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { RowsCollpsedIcon, RowsExpandedIcon } from 'nft/components/icons'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { ListingMarket, ListingWarning, WalletAsset } from 'nft/types'
|
||||
import { LOOKS_RARE_CREATOR_BASIS_POINTS } from 'nft/utils'
|
||||
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
|
||||
import { fetchPrice } from 'nft/utils/fetchPrice'
|
||||
import { Dispatch, useEffect, useMemo, useState } from 'react'
|
||||
import React, { Dispatch, DispatchWithoutAction, useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
|
||||
@@ -22,26 +23,51 @@ const PastPriceInfo = styled(Column)`
|
||||
display: none;
|
||||
flex: 1;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.xxl}px) {
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
|
||||
const RemoveMarketplaceWrap = styled(RemoveIconWrap)`
|
||||
top: 11px;
|
||||
top: 8px;
|
||||
left: 16px;
|
||||
z-index: 3;
|
||||
`
|
||||
|
||||
const MarketIconsWrapper = styled(Row)`
|
||||
position: relative;
|
||||
margin-right: 12px;
|
||||
width: 44px;
|
||||
justify-content: flex-end;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const MarketIconWrapper = styled(Column)`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-right: 16px;
|
||||
`
|
||||
|
||||
const MarketIcon = styled.img`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
const MarketIcon = styled.img<{ index: number }>`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
z-index: ${({ index }) => 2 - index};
|
||||
margin-left: ${({ index }) => `${index === 0 ? 0 : -8}px`};
|
||||
outline: 1px solid ${({ theme }) => theme.backgroundInteractive};
|
||||
`
|
||||
|
||||
const ExpandMarketIconWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
height: 28px;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const FeeColumnWrapper = styled(Column)`
|
||||
@@ -49,7 +75,7 @@ const FeeColumnWrapper = styled(Column)`
|
||||
align-items: flex-end;
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
@@ -63,7 +89,7 @@ const ReturnColumn = styled(Column)`
|
||||
flex: 1.5;
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
@@ -84,6 +110,8 @@ interface MarketplaceRowProps {
|
||||
asset: WalletAsset
|
||||
showMarketplaceLogo: boolean
|
||||
expandMarketplaceRows?: boolean
|
||||
rowHovered?: boolean
|
||||
toggleExpandMarketplaceRows: DispatchWithoutAction
|
||||
}
|
||||
|
||||
export const MarketplaceRow = ({
|
||||
@@ -95,14 +123,16 @@ export const MarketplaceRow = ({
|
||||
asset,
|
||||
showMarketplaceLogo,
|
||||
expandMarketplaceRows,
|
||||
toggleExpandMarketplaceRows,
|
||||
rowHovered,
|
||||
}: MarketplaceRowProps) => {
|
||||
const [listPrice, setListPrice] = useState<number>()
|
||||
const [globalOverride, setGlobalOverride] = useState(false)
|
||||
const showGlobalPrice = globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride && globalPrice
|
||||
const setAssetListPrice = useSellAsset((state) => state.setAssetListPrice)
|
||||
const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const handleHover = () => setHovered(!hovered)
|
||||
const [marketIconHovered, toggleMarketIconHovered] = useReducer((s) => !s, false)
|
||||
const [marketRowHovered, toggleMarketRowHovered] = useReducer((s) => !s, false)
|
||||
|
||||
const price = showGlobalPrice ? globalPrice : listPrice
|
||||
|
||||
@@ -135,7 +165,7 @@ export const MarketplaceRow = ({
|
||||
if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) {
|
||||
setListPrice(asset?.floorPrice)
|
||||
setGlobalPrice(asset.floorPrice)
|
||||
} else if (globalPriceMethod === SetPriceMethod.PREV_LISTING) {
|
||||
} else if (globalPriceMethod === SetPriceMethod.LAST_PRICE) {
|
||||
setListPrice(asset.lastPrice)
|
||||
setGlobalPrice(asset.lastPrice)
|
||||
} else if (globalPriceMethod === SetPriceMethod.SAME_PRICE)
|
||||
@@ -186,7 +216,7 @@ export const MarketplaceRow = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Row onMouseEnter={toggleMarketRowHovered} onMouseLeave={toggleMarketRowHovered}>
|
||||
<PastPriceInfo>
|
||||
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">
|
||||
{asset.floorPrice ? `${asset.floorPrice.toFixed(3)} ETH` : '-'}
|
||||
@@ -198,22 +228,25 @@ export const MarketplaceRow = ({
|
||||
</ThemedText.BodySmall>
|
||||
</PastPriceInfo>
|
||||
|
||||
<Row flex="2">
|
||||
{showMarketplaceLogo && (
|
||||
<MarketIconWrapper
|
||||
onMouseEnter={handleHover}
|
||||
onMouseLeave={handleHover}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeAssetMarketplace(asset, selectedMarkets[0])
|
||||
removeMarket && removeMarket()
|
||||
}}
|
||||
>
|
||||
<MarketIcon alt={selectedMarkets[0].name} src={selectedMarkets[0].icon} />
|
||||
<RemoveMarketplaceWrap hovered={hovered}>
|
||||
<img width="32px" src="/nft/svgs/minusCircle.svg" alt="Remove item" />
|
||||
</RemoveMarketplaceWrap>
|
||||
</MarketIconWrapper>
|
||||
<Row flex="3">
|
||||
{(expandMarketplaceRows || selectedMarkets.length > 1) && (
|
||||
<MarketIconsWrapper onMouseEnter={toggleMarketIconHovered} onMouseLeave={toggleMarketIconHovered}>
|
||||
{selectedMarkets.map((market, index) => (
|
||||
<MarketIconWrapper
|
||||
key={market.name + asset.collection?.address + asset.tokenId}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeAssetMarketplace(asset, selectedMarkets[0])
|
||||
removeMarket && removeMarket()
|
||||
}}
|
||||
>
|
||||
<MarketIcon alt={selectedMarkets[0].name} src={market.icon} index={index} />
|
||||
<RemoveMarketplaceWrap hovered={marketIconHovered && (expandMarketplaceRows ?? false)}>
|
||||
<img width="20px" src="/nft/svgs/minusCircle.svg" alt="Remove item" />
|
||||
</RemoveMarketplaceWrap>
|
||||
</MarketIconWrapper>
|
||||
))}
|
||||
</MarketIconsWrapper>
|
||||
)}
|
||||
{globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride ? (
|
||||
<PriceTextInput
|
||||
@@ -224,7 +257,6 @@ export const MarketplaceRow = ({
|
||||
globalOverride={globalOverride}
|
||||
warning={warning}
|
||||
asset={asset}
|
||||
shrink={expandMarketplaceRows}
|
||||
/>
|
||||
) : (
|
||||
<PriceTextInput
|
||||
@@ -235,9 +267,13 @@ export const MarketplaceRow = ({
|
||||
globalOverride={globalOverride}
|
||||
warning={warning}
|
||||
asset={asset}
|
||||
shrink={expandMarketplaceRows}
|
||||
/>
|
||||
)}
|
||||
{rowHovered && ((expandMarketplaceRows && marketRowHovered) || selectedMarkets.length > 1) && (
|
||||
<ExpandMarketIconWrapper onClick={toggleExpandMarketplaceRows}>
|
||||
{expandMarketplaceRows ? <RowsExpandedIcon /> : <RowsCollpsedIcon />}
|
||||
</ExpandMarketIconWrapper>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<FeeColumnWrapper>
|
||||
|
||||
124
src/nft/components/profile/list/Modal/BelowFloorWarningModal.tsx
Normal file
124
src/nft/components/profile/list/Modal/BelowFloorWarningModal.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Plural, t, Trans } from '@lingui/macro'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import Column from 'components/Column'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import { Listing, WalletAsset } from 'nft/types'
|
||||
import React from 'react'
|
||||
import { AlertTriangle, X } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
const ModalWrapper = styled(Column)`
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 420px;
|
||||
z-index: ${Z_INDEX.modal};
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
padding: 20px 24px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
const CloseIconWrapper = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
const CloseIcon = styled(X)`
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const HazardIconWrap = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 32px 120px;
|
||||
`
|
||||
|
||||
const ContinueButton = styled(ButtonPrimary)`
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
margin-top: 12px;
|
||||
`
|
||||
|
||||
const EditListings = styled.span`
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 12px 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
export const BelowFloorWarningModal = ({
|
||||
listingsBelowFloor,
|
||||
closeModal,
|
||||
startListing,
|
||||
}: {
|
||||
listingsBelowFloor: [WalletAsset, Listing][]
|
||||
closeModal: () => void
|
||||
startListing: () => void
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const clickContinue = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
startListing()
|
||||
closeModal()
|
||||
}
|
||||
return (
|
||||
<Portal>
|
||||
<ModalWrapper>
|
||||
<CloseIconWrapper>
|
||||
<CloseIcon width={24} height={24} onClick={closeModal} />{' '}
|
||||
</CloseIconWrapper>
|
||||
<HazardIconWrap>
|
||||
<AlertTriangle height={90} width={90} color={theme.accentCritical} />
|
||||
</HazardIconWrap>
|
||||
<ThemedText.HeadlineSmall lineHeight="28px" textAlign="center">
|
||||
<Trans>Low listing price</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ThemedText.BodyPrimary textAlign="center">
|
||||
<Plural
|
||||
value={listingsBelowFloor.length !== 1 ? 2 : 1}
|
||||
_1={t`One NFT is listed ${(
|
||||
(1 - (listingsBelowFloor[0][1].price ?? 0) / (listingsBelowFloor[0][0].floorPrice ?? 0)) *
|
||||
100
|
||||
).toFixed(0)}% `}
|
||||
other={t`${listingsBelowFloor.length} NFTs are listed significantly `}
|
||||
/>
|
||||
|
||||
<Trans>below the collection’s floor price. Are you sure you want to continue?</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
<ContinueButton onClick={clickContinue}>
|
||||
<Trans>Continue</Trans>
|
||||
</ContinueButton>
|
||||
<EditListings onClick={closeModal}>
|
||||
<Trans>Edit listings</Trans>
|
||||
</EditListings>
|
||||
</ModalWrapper>
|
||||
<Overlay onClick={closeModal} />
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
181
src/nft/components/profile/list/Modal/ContentRow.tsx
Normal file
181
src/nft/components/profile/list/Modal/ContentRow.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import Loader from 'components/Loader'
|
||||
import Row from 'components/Row'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { AssetRow, CollectionRow, ListingStatus } from 'nft/types'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Check, XOctagon } from 'react-feather'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { opacify } from 'theme/utils'
|
||||
|
||||
const ContentColumn = styled(Column)<{ failed: boolean }>`
|
||||
background-color: ${({ theme, failed }) => failed && opacify(12, theme.accentCritical)};
|
||||
border-radius: 12px;
|
||||
padding-bottom: ${({ failed }) => failed && '16px'};
|
||||
`
|
||||
|
||||
const ContentRowWrapper = styled(Row)<{ active: boolean; failed: boolean }>`
|
||||
padding: 16px;
|
||||
border: ${({ failed, theme }) => !failed && `1px solid ${theme.backgroundOutline}`};
|
||||
border-radius: 12px;
|
||||
opacity: ${({ active, failed }) => (active || failed ? '1' : '0.6')};
|
||||
`
|
||||
|
||||
const CollectionIcon = styled.img`
|
||||
border-radius: 100px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const AssetIcon = styled.img`
|
||||
border-radius: 4px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const MarketplaceIcon = styled.img`
|
||||
border-radius: 4px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-left: -4px;
|
||||
margin-right: 12px;
|
||||
`
|
||||
|
||||
const ContentName = styled(ThemedText.SubHeaderSmall)`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 40%;
|
||||
`
|
||||
|
||||
const ProceedText = styled.span`
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const FailedText = styled.span`
|
||||
font-weight: 600;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
color: ${({ theme }) => theme.accentCritical};
|
||||
margin-left: 4px;
|
||||
`
|
||||
|
||||
const StyledVerifiedIcon = styled(VerifiedIcon)`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: 4px;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
margin-left: auto;
|
||||
margin-right: 0px;
|
||||
`
|
||||
|
||||
const ButtonRow = styled(Row)`
|
||||
padding: 0px 16px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const failedButtonStyle = css`
|
||||
width: 152px;
|
||||
cursor: pointer;
|
||||
padding: 8px 0px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const RemoveButton = styled.button`
|
||||
background-color: ${({ theme }) => theme.accentCritical};
|
||||
color: ${({ theme }) => theme.accentTextDarkPrimary};
|
||||
${failedButtonStyle}
|
||||
`
|
||||
|
||||
const RetryButton = styled.button`
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
${failedButtonStyle}
|
||||
`
|
||||
|
||||
export const ContentRow = ({
|
||||
row,
|
||||
isCollectionApprovalSection,
|
||||
removeRow,
|
||||
}: {
|
||||
row: AssetRow
|
||||
isCollectionApprovalSection: boolean
|
||||
removeRow: (row: AssetRow) => void
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const rowRef = useRef<HTMLDivElement>()
|
||||
const failed = row.status === ListingStatus.FAILED || row.status === ListingStatus.REJECTED
|
||||
|
||||
useEffect(() => {
|
||||
row.status === ListingStatus.SIGNING && rowRef.current?.scroll
|
||||
}, [row.status])
|
||||
|
||||
return (
|
||||
<ContentColumn failed={failed}>
|
||||
<ContentRowWrapper
|
||||
active={row.status === ListingStatus.SIGNING || row.status === ListingStatus.APPROVED}
|
||||
failed={failed}
|
||||
ref={rowRef}
|
||||
>
|
||||
{isCollectionApprovalSection ? <CollectionIcon src={row.images[0]} /> : <AssetIcon src={row.images[0]} />}
|
||||
<MarketplaceIcon src={row.images[1]} />
|
||||
<ContentName>{row.name}</ContentName>
|
||||
{isCollectionApprovalSection && (row as CollectionRow).isVerified && <StyledVerifiedIcon />}
|
||||
<IconWrapper>
|
||||
{row.status === ListingStatus.DEFINED || row.status === ListingStatus.PENDING ? (
|
||||
<Loader
|
||||
height="14px"
|
||||
width="14px"
|
||||
stroke={row.status === ListingStatus.PENDING ? theme.accentAction : theme.textTertiary}
|
||||
/>
|
||||
) : row.status === ListingStatus.SIGNING ? (
|
||||
<ProceedText>
|
||||
<Trans>Proceed in wallet</Trans>
|
||||
</ProceedText>
|
||||
) : row.status === ListingStatus.APPROVED ? (
|
||||
<Check height="20" width="20" stroke={theme.accentSuccess} />
|
||||
) : (
|
||||
failed && (
|
||||
<Row>
|
||||
<XOctagon height="20" width="20" color={theme.accentCritical} />
|
||||
<FailedText>
|
||||
{row.status === ListingStatus.FAILED ? <Trans>Failed</Trans> : <Trans>Rejected</Trans>}
|
||||
</FailedText>
|
||||
</Row>
|
||||
)
|
||||
)}
|
||||
</IconWrapper>
|
||||
</ContentRowWrapper>
|
||||
{failed && (
|
||||
<ButtonRow justify="space-between">
|
||||
<RemoveButton onClick={() => removeRow(row)}>
|
||||
<Trans>Remove</Trans>
|
||||
</RemoveButton>
|
||||
<RetryButton onClick={row.callback}>
|
||||
<Trans>Retry</Trans>
|
||||
</RetryButton>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</ContentColumn>
|
||||
)
|
||||
}
|
||||
153
src/nft/components/profile/list/Modal/ListModal.tsx
Normal file
153
src/nft/components/profile/list/Modal/ListModal.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getTotalEthValue, signListingRow } from 'nft/components/bag/profile/utils'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import { useNFTList, useSellAsset } from 'nft/hooks'
|
||||
import { ListingStatus } from 'nft/types'
|
||||
import { fetchPrice } from 'nft/utils'
|
||||
import { useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import { TitleRow } from '../shared'
|
||||
import { ListModalSection, Section } from './ListModalSection'
|
||||
import { SuccessScreen } from './SuccessScreen'
|
||||
|
||||
const ListModalWrapper = styled.div`
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 420px;
|
||||
z-index: ${Z_INDEX.modal};
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
padding: 20px 24px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
|
||||
const { provider } = useWeb3React()
|
||||
const signer = provider?.getSigner()
|
||||
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const {
|
||||
listingStatus,
|
||||
setListingStatusAndCallback,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
} = useNFTList(
|
||||
({
|
||||
listingStatus,
|
||||
setListingStatusAndCallback,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}) => ({
|
||||
listingStatus,
|
||||
setListingStatusAndCallback,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
|
||||
const [openSection, toggleOpenSection] = useReducer(
|
||||
(s) => (s === Section.APPROVE ? Section.SIGN : Section.APPROVE),
|
||||
Section.APPROVE
|
||||
)
|
||||
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrice().then((price) => {
|
||||
setEthPriceInUSD(price || 0)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const allCollectionsApproved = useMemo(
|
||||
() => collectionsRequiringApproval.every((collection) => collection.status === ListingStatus.APPROVED),
|
||||
[collectionsRequiringApproval]
|
||||
)
|
||||
|
||||
const signListings = async () => {
|
||||
if (!signer || !provider) return
|
||||
// sign listings
|
||||
for (const listing of listings) {
|
||||
await signListingRow(listing, signer, provider, getLooksRareNonce, setLooksRareNonce, setListingStatusAndCallback)
|
||||
}
|
||||
sendAnalyticsEvent(NFTEventName.NFT_LISTING_COMPLETED, {
|
||||
signatures_approved: listings.filter((asset) => asset.status === ListingStatus.APPROVED),
|
||||
list_quantity: listings.length,
|
||||
usd_value: ethPriceInUSD * totalEthListingValue,
|
||||
...trace,
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (allCollectionsApproved) {
|
||||
signListings()
|
||||
openSection === Section.APPROVE && toggleOpenSection()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allCollectionsApproved])
|
||||
|
||||
// In the case that a user removes all listings via retry logic, close modal
|
||||
useEffect(() => {
|
||||
!listings.length && overlayClick()
|
||||
}, [listings, overlayClick])
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Trace modal={InterfaceModalName.NFT_LISTING}>
|
||||
<ListModalWrapper>
|
||||
{listingStatus === ListingStatus.APPROVED ? (
|
||||
<SuccessScreen overlayClick={overlayClick} />
|
||||
) : (
|
||||
<>
|
||||
<TitleRow>
|
||||
<ThemedText.HeadlineSmall lineHeight="28px">
|
||||
<Trans>List NFTs</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
<X size={24} cursor="pointer" onClick={overlayClick} />
|
||||
</TitleRow>
|
||||
<ListModalSection
|
||||
sectionType={Section.APPROVE}
|
||||
active={openSection === Section.APPROVE}
|
||||
content={collectionsRequiringApproval}
|
||||
toggleSection={toggleOpenSection}
|
||||
/>
|
||||
<ListModalSection
|
||||
sectionType={Section.SIGN}
|
||||
active={openSection === Section.SIGN}
|
||||
content={listings}
|
||||
toggleSection={toggleOpenSection}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ListModalWrapper>
|
||||
</Trace>
|
||||
<Overlay onClick={overlayClick} />
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
148
src/nft/components/profile/list/Modal/ListModalSection.tsx
Normal file
148
src/nft/components/profile/list/Modal/ListModalSection.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import { ScrollBarStyles } from 'components/Common'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { ChevronUpIcon, ListingModalWindowActive, ListingModalWindowClosed } from 'nft/components/icons'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
import { Info } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { colors } from 'theme/colors'
|
||||
import { TRANSITION_DURATIONS } from 'theme/styles'
|
||||
|
||||
import { ContentRow } from './ContentRow'
|
||||
|
||||
const SectionHeader = styled(Row)`
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const SectionTitle = styled(ThemedText.SubHeader)<{ active: boolean; approved: boolean }>`
|
||||
line-height: 24px;
|
||||
color: ${({ theme, active, approved }) =>
|
||||
approved ? theme.accentSuccess : active ? theme.textPrimary : theme.textSecondary};
|
||||
`
|
||||
|
||||
const SectionArrow = styled(ChevronUpIcon)<{ active: boolean }>`
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
cursor: pointer;
|
||||
transition: ${TRANSITION_DURATIONS.medium}ms;
|
||||
transform: rotate(${({ active }) => (active ? 0 : 180)}deg);
|
||||
`
|
||||
|
||||
const SectionBody = styled(Column)`
|
||||
border-left: 1.5px solid ${colors.gray650};
|
||||
margin-top: 4px;
|
||||
margin-left: 7px;
|
||||
padding-top: 4px;
|
||||
padding-left: 20px;
|
||||
max-height: 394px;
|
||||
overflow-y: auto;
|
||||
${ScrollBarStyles}
|
||||
`
|
||||
|
||||
const StyledInfoIcon = styled(Info)`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: 4px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const ContentRowContainer = styled(Column)`
|
||||
gap: 8px;
|
||||
scroll-behavior: smooth;
|
||||
`
|
||||
|
||||
export const enum Section {
|
||||
APPROVE,
|
||||
SIGN,
|
||||
}
|
||||
|
||||
interface ListModalSectionProps {
|
||||
sectionType: Section
|
||||
active: boolean
|
||||
content: AssetRow[]
|
||||
toggleSection: React.DispatchWithoutAction
|
||||
}
|
||||
|
||||
export const ListModalSection = ({ sectionType, active, content, toggleSection }: ListModalSectionProps) => {
|
||||
const theme = useTheme()
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace)
|
||||
const allContentApproved = useMemo(() => !content.some((row) => row.status !== ListingStatus.APPROVED), [content])
|
||||
const isCollectionApprovalSection = sectionType === Section.APPROVE
|
||||
const removeRow = (row: AssetRow) => {
|
||||
// collections
|
||||
if (isCollectionApprovalSection) {
|
||||
const collectionRow = row as CollectionRow
|
||||
for (const asset of sellAssets)
|
||||
if (asset.asset_contract.address === collectionRow.collectionAddress)
|
||||
removeAssetMarketplace(asset, collectionRow.marketplace)
|
||||
}
|
||||
// listings
|
||||
else {
|
||||
const listingRow = row as ListingRow
|
||||
removeAssetMarketplace(listingRow.asset, listingRow.marketplace)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Column>
|
||||
<SectionHeader>
|
||||
<Row>
|
||||
{active || allContentApproved ? (
|
||||
<ListingModalWindowActive fill={allContentApproved ? theme.accentSuccess : theme.accentAction} />
|
||||
) : (
|
||||
<ListingModalWindowClosed />
|
||||
)}
|
||||
<SectionTitle active={active} marginLeft="12px" approved={allContentApproved}>
|
||||
{isCollectionApprovalSection ? (
|
||||
<>
|
||||
<Trans>Approve</Trans> {content.length}
|
||||
<Plural value={content.length} _1="Collection" other="Collections" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trans>Sign</Trans> {content.length} {' '}
|
||||
<Plural value={content.length} _1="Listing" other="Listings" />
|
||||
</>
|
||||
)}
|
||||
</SectionTitle>
|
||||
</Row>
|
||||
<SectionArrow
|
||||
active={active}
|
||||
secondaryColor={active ? theme.textPrimary : theme.textSecondary}
|
||||
onClick={toggleSection}
|
||||
/>
|
||||
</SectionHeader>
|
||||
{active && (
|
||||
<SectionBody>
|
||||
{isCollectionApprovalSection && (
|
||||
<Row height="16px" marginBottom="16px">
|
||||
<ThemedText.Caption lineHeight="16px" color="textSecondary">
|
||||
<Trans>Why is a transaction required?</Trans>
|
||||
</ThemedText.Caption>
|
||||
<MouseoverTooltip
|
||||
text={<Trans>Listing an NFT requires a one-time marketplace approval for each NFT collection.</Trans>}
|
||||
>
|
||||
<StyledInfoIcon />
|
||||
</MouseoverTooltip>
|
||||
</Row>
|
||||
)}
|
||||
<ContentRowContainer>
|
||||
{content.map((row: AssetRow) => (
|
||||
<ContentRow
|
||||
row={row}
|
||||
key={(row?.name ?? '') + row?.images[1]}
|
||||
removeRow={removeRow}
|
||||
isCollectionApprovalSection={isCollectionApprovalSection}
|
||||
/>
|
||||
))}
|
||||
</ContentRowContainer>
|
||||
</SectionBody>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
129
src/nft/components/profile/list/Modal/SuccessScreen.tsx
Normal file
129
src/nft/components/profile/list/Modal/SuccessScreen.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import Column from 'components/Column'
|
||||
import { ScrollBarStyles } from 'components/Common'
|
||||
import Row from 'components/Row'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { getTotalEthValue } from 'nft/components/bag/profile/utils'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { formatEth, generateTweetForList, pluralize } from 'nft/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { Twitter, X } from 'react-feather'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
|
||||
import { TitleRow } from '../shared'
|
||||
|
||||
const SuccessImage = styled.img<{ numImages: number }>`
|
||||
width: calc(${({ numImages }) => (numImages > 1 ? (numImages > 2 ? '33%' : '50%') : '100%')} - 12px);
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
const SuccessImageWrapper = styled(Row)`
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
overflow-y: scroll;
|
||||
margin-bottom: 16px;
|
||||
${ScrollBarStyles}
|
||||
`
|
||||
|
||||
const ProceedsColumn = styled(Column)`
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const buttonStyle = css`
|
||||
width: 182px;
|
||||
cursor: pointer;
|
||||
padding: 12px 0px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const ReturnButton = styled.button`
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
${buttonStyle}
|
||||
`
|
||||
|
||||
const TweetButton = styled.a`
|
||||
background-color: ${({ theme }) => theme.accentAction};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
text-decoration: none;
|
||||
${buttonStyle}
|
||||
`
|
||||
|
||||
const TweetRow = styled(Row)`
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
export const SuccessScreen = ({ overlayClick }: { overlayClick: () => void }) => {
|
||||
const theme = useTheme()
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
|
||||
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
|
||||
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
|
||||
const usdcValue = useStablecoinValue(parsedAmount)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleRow>
|
||||
<ThemedText.HeadlineSmall lineHeight="28px">
|
||||
<Trans>Successfully listed</Trans> {sellAssets.length > 1 ? ` ${sellAssets.length} ` : ''}
|
||||
NFT{pluralize(sellAssets.length)}!
|
||||
</ThemedText.HeadlineSmall>
|
||||
<X size={24} cursor="pointer" onClick={overlayClick} />
|
||||
</TitleRow>
|
||||
<SuccessImageWrapper>
|
||||
{sellAssets.map((asset) => (
|
||||
<SuccessImage
|
||||
src={asset.imageUrl}
|
||||
key={asset?.asset_contract?.address ?? '' + asset?.tokenId}
|
||||
numImages={sellAssets.length}
|
||||
/>
|
||||
))}
|
||||
</SuccessImageWrapper>
|
||||
<Row justify="space-between" align="flex-start" marginBottom="16px">
|
||||
<ThemedText.SubHeader lineHeight="24px">
|
||||
<Trans>Proceeds if sold</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<ProceedsColumn>
|
||||
<ThemedText.SubHeader lineHeight="24px">{formatEth(totalEthListingValue)} ETH</ThemedText.SubHeader>
|
||||
{usdcValue && (
|
||||
<ThemedText.BodySmall lineHeight="20px" color="textSecondary">
|
||||
{formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)}
|
||||
</ThemedText.BodySmall>
|
||||
)}
|
||||
</ProceedsColumn>
|
||||
</Row>
|
||||
<Row justify="space-between" flexWrap="wrap">
|
||||
<ReturnButton onClick={() => window.location.reload()}>
|
||||
<Trans>Return to My NFTs</Trans>
|
||||
</ReturnButton>
|
||||
<TweetButton href={generateTweetForList(sellAssets)} target="_blank" rel="noreferrer">
|
||||
<TweetRow>
|
||||
<Twitter height={20} width={20} color={theme.textPrimary} fill={theme.textPrimary} />
|
||||
<Trans>Share on Twitter</Trans>
|
||||
</TweetRow>
|
||||
</TweetButton>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,53 +1,56 @@
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { RowsCollpsedIcon, RowsExpandedIcon, VerifiedIcon } from 'nft/components/icons'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { ListingMarket, WalletAsset } from 'nft/types'
|
||||
import { Dispatch, useEffect, useState } from 'react'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { Dispatch, useEffect, useReducer, useState } from 'react'
|
||||
import { Trash2 } from 'react-feather'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
import { opacify } from 'theme/utils'
|
||||
|
||||
import { MarketplaceRow } from './MarketplaceRow'
|
||||
import { SetPriceMethod } from './NFTListingsGrid'
|
||||
import { RemoveIconWrap } from './shared'
|
||||
|
||||
const NFTListRowWrapper = styled(Row)`
|
||||
margin: 24px 0px;
|
||||
padding: 24px 0px;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => opacify(24, theme.backgroundOutline)};
|
||||
}
|
||||
`
|
||||
|
||||
const RemoveIconContainer = styled.div`
|
||||
width: 48px;
|
||||
height: 44px;
|
||||
padding-left: 12px;
|
||||
align-self: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
`
|
||||
|
||||
const NFTInfoWrapper = styled(Row)`
|
||||
align-items: center;
|
||||
min-width: 0px;
|
||||
flex: 2;
|
||||
flex: 1.5;
|
||||
margin-bottom: auto;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
flex: 1.5;
|
||||
}
|
||||
`
|
||||
|
||||
const ExpandMarketIconWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const NFTImageWrapper = styled.div`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const NFTImage = styled.img`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const RemoveIcon = styled.img`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const HideTextOverflow = css`
|
||||
@@ -77,7 +80,12 @@ const CollectionName = styled(ThemedText.BodySmall)`
|
||||
|
||||
const MarketPlaceRowWrapper = styled(Column)`
|
||||
gap: 24px;
|
||||
flex: 1;
|
||||
flex: 1.5;
|
||||
margin-right: 12px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
flex: 3;
|
||||
@@ -103,37 +111,41 @@ export const NFTListRow = ({
|
||||
setGlobalPrice,
|
||||
selectedMarkets,
|
||||
}: NFTListRowProps) => {
|
||||
const [expandMarketplaceRows, setExpandMarketplaceRows] = useState(false)
|
||||
const [expandMarketplaceRows, toggleExpandMarketplaceRows] = useReducer((s) => !s, false)
|
||||
const removeAsset = useSellAsset((state) => state.removeSellAsset)
|
||||
const [localMarkets, setLocalMarkets] = useState([])
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const handleHover = () => setHovered(!hovered)
|
||||
const [localMarkets, setLocalMarkets] = useState<ListingMarket[]>([])
|
||||
const [hovered, toggleHovered] = useReducer((s) => !s, false)
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setLocalMarkets(JSON.parse(JSON.stringify(selectedMarkets)))
|
||||
selectedMarkets.length < 2 && setExpandMarketplaceRows(false)
|
||||
}, [selectedMarkets])
|
||||
selectedMarkets.length < 2 && expandMarketplaceRows && toggleExpandMarketplaceRows()
|
||||
}, [expandMarketplaceRows, selectedMarkets])
|
||||
|
||||
return (
|
||||
<NFTListRowWrapper>
|
||||
<NFTInfoWrapper>
|
||||
{localMarkets.length > 1 && (
|
||||
<ExpandMarketIconWrapper onClick={() => setExpandMarketplaceRows(!expandMarketplaceRows)}>
|
||||
{expandMarketplaceRows ? <RowsExpandedIcon /> : <RowsCollpsedIcon />}
|
||||
</ExpandMarketIconWrapper>
|
||||
<NFTListRowWrapper
|
||||
onMouseEnter={() => {
|
||||
!hovered && toggleHovered()
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
hovered && toggleHovered()
|
||||
}}
|
||||
>
|
||||
<RemoveIconContainer>
|
||||
{hovered && (
|
||||
<Trash2
|
||||
size={20}
|
||||
color={theme.textSecondary}
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
removeAsset(asset)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NFTImageWrapper
|
||||
onMouseEnter={handleHover}
|
||||
onMouseLeave={handleHover}
|
||||
onClick={() => {
|
||||
removeAsset(asset)
|
||||
}}
|
||||
>
|
||||
<RemoveIconWrap hovered={hovered}>
|
||||
<RemoveIcon src="/nft/svgs/minusCircle.svg" alt="Remove item" />
|
||||
</RemoveIconWrap>
|
||||
<NFTImage alt={asset.name} src={asset.imageUrl || '/nft/svgs/image-placeholder.svg'} />
|
||||
</NFTImageWrapper>
|
||||
</RemoveIconContainer>
|
||||
|
||||
<NFTInfoWrapper>
|
||||
<NFTImage alt={asset.name} src={asset.imageUrl || '/nft/svgs/image-placeholder.svg'} />
|
||||
<TokenInfoWrapper>
|
||||
<TokenName>{asset.name ? asset.name : `#${asset.tokenId}`}</TokenName>
|
||||
<CollectionName>
|
||||
@@ -154,8 +166,10 @@ export const NFTListRow = ({
|
||||
removeMarket={() => localMarkets.splice(index, 1)}
|
||||
asset={asset}
|
||||
showMarketplaceLogo={true}
|
||||
key={index}
|
||||
key={asset.name + market.name}
|
||||
expandMarketplaceRows={expandMarketplaceRows}
|
||||
rowHovered={hovered}
|
||||
toggleExpandMarketplaceRows={toggleExpandMarketplaceRows}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -167,6 +181,8 @@ export const NFTListRow = ({
|
||||
selectedMarkets={localMarkets}
|
||||
asset={asset}
|
||||
showMarketplaceLogo={false}
|
||||
rowHovered={hovered}
|
||||
toggleExpandMarketplaceRows={toggleExpandMarketplaceRows}
|
||||
/>
|
||||
)}
|
||||
</MarketPlaceRowWrapper>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { SortDropdown } from 'nft/components/common/SortDropdown'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { DropDownOption, ListingMarket } from 'nft/types'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo, useReducer, useRef, useState } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
import { Dropdown } from './Dropdown'
|
||||
import { NFTListRow } from './NFTListRow'
|
||||
|
||||
const TableHeader = styled.div`
|
||||
@@ -25,18 +26,19 @@ const TableHeader = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 20px;
|
||||
`
|
||||
|
||||
const NFTHeader = styled.div`
|
||||
flex: 2;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
flex: 1.5;
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
margin-left: 48px;
|
||||
}
|
||||
`
|
||||
|
||||
const NFTHeader = styled.div`
|
||||
flex: 1.5;
|
||||
`
|
||||
|
||||
const PriceHeaders = styled(Row)`
|
||||
flex: 1;
|
||||
flex: 1.5;
|
||||
margin-right: 12px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
flex: 3;
|
||||
@@ -52,14 +54,58 @@ const PriceInfoHeader = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const DropdownWrapper = styled.div`
|
||||
flex: 2;
|
||||
const DropdownAndHeaderWrapper = styled(Row)`
|
||||
flex: 3;
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
const DropdownPromptContainer = styled(Column)`
|
||||
position: relative;
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const DropdownPrompt = styled(Row)`
|
||||
gap: 4px;
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
width: min-content;
|
||||
white-space: nowrap;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
|
||||
&:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
`
|
||||
|
||||
const DropdownChevron = styled(ChevronDown)<{ isOpen: boolean }>`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
transform: ${({ isOpen }) => isOpen && 'rotate(180deg)'};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `transform ${duration.fast} ${timing.ease}`};
|
||||
`
|
||||
|
||||
const DropdownContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
right: 0px;
|
||||
`
|
||||
|
||||
const FeeUserReceivesSharedStyles = css`
|
||||
display: none;
|
||||
justify-content: flex-end;
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
@@ -80,38 +126,81 @@ const RowDivider = styled.hr`
|
||||
border-radius: 20px;
|
||||
border-width: 0.5px;
|
||||
border-style: solid;
|
||||
margin: 0;
|
||||
border-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
`
|
||||
|
||||
export enum SetPriceMethod {
|
||||
SAME_PRICE,
|
||||
FLOOR_PRICE,
|
||||
PREV_LISTING,
|
||||
LAST_PRICE,
|
||||
CUSTOM,
|
||||
}
|
||||
|
||||
export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingMarket[] }) => {
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const [globalPriceMethod, setGlobalPriceMethod] = useState<SetPriceMethod>()
|
||||
const [globalPriceMethod, setGlobalPriceMethod] = useState(SetPriceMethod.CUSTOM)
|
||||
const [globalPrice, setGlobalPrice] = useState<number>()
|
||||
const [showDropdown, toggleShowDropdown] = useReducer((s) => !s, false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(dropdownRef, showDropdown ? toggleShowDropdown : undefined)
|
||||
|
||||
const priceDropdownOptions: DropDownOption[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
displayText: 'Same price',
|
||||
onClick: () => setGlobalPriceMethod(SetPriceMethod.SAME_PRICE),
|
||||
displayText: 'Custom',
|
||||
isSelected: globalPriceMethod === SetPriceMethod.CUSTOM,
|
||||
onClick: () => {
|
||||
setGlobalPriceMethod(SetPriceMethod.CUSTOM)
|
||||
toggleShowDropdown()
|
||||
},
|
||||
},
|
||||
{
|
||||
displayText: 'Floor price',
|
||||
onClick: () => setGlobalPriceMethod(SetPriceMethod.FLOOR_PRICE),
|
||||
isSelected: globalPriceMethod === SetPriceMethod.FLOOR_PRICE,
|
||||
onClick: () => {
|
||||
setGlobalPriceMethod(SetPriceMethod.FLOOR_PRICE)
|
||||
toggleShowDropdown()
|
||||
},
|
||||
},
|
||||
{
|
||||
displayText: 'Prev. listing',
|
||||
onClick: () => setGlobalPriceMethod(SetPriceMethod.PREV_LISTING),
|
||||
displayText: 'Last price',
|
||||
isSelected: globalPriceMethod === SetPriceMethod.LAST_PRICE,
|
||||
onClick: () => {
|
||||
setGlobalPriceMethod(SetPriceMethod.LAST_PRICE)
|
||||
toggleShowDropdown()
|
||||
},
|
||||
},
|
||||
{
|
||||
displayText: 'Same price',
|
||||
isSelected: globalPriceMethod === SetPriceMethod.SAME_PRICE,
|
||||
onClick: () => {
|
||||
setGlobalPriceMethod(SetPriceMethod.SAME_PRICE)
|
||||
toggleShowDropdown()
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
[globalPriceMethod]
|
||||
)
|
||||
|
||||
let prompt
|
||||
switch (globalPriceMethod) {
|
||||
case SetPriceMethod.CUSTOM:
|
||||
prompt = <Trans>Custom</Trans>
|
||||
break
|
||||
case SetPriceMethod.FLOOR_PRICE:
|
||||
prompt = <Trans>Floor price</Trans>
|
||||
break
|
||||
case SetPriceMethod.LAST_PRICE:
|
||||
prompt = <Trans>Last Price</Trans>
|
||||
break
|
||||
case SetPriceMethod.SAME_PRICE:
|
||||
prompt = <Trans>Same Price</Trans>
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<TableHeader>
|
||||
@@ -126,9 +215,19 @@ export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingM
|
||||
<Trans>Last</Trans>
|
||||
</PriceInfoHeader>
|
||||
|
||||
<DropdownWrapper>
|
||||
<SortDropdown dropDownOptions={priceDropdownOptions} mini miniPrompt={t`Set price by`} />
|
||||
</DropdownWrapper>
|
||||
<DropdownAndHeaderWrapper ref={dropdownRef}>
|
||||
<Trans>Price</Trans>
|
||||
<DropdownPromptContainer>
|
||||
<DropdownPrompt onClick={toggleShowDropdown}>
|
||||
{prompt} <DropdownChevron isOpen={showDropdown} />
|
||||
</DropdownPrompt>
|
||||
{showDropdown && (
|
||||
<DropdownContainer>
|
||||
<Dropdown dropDownOptions={priceDropdownOptions} width={200} />
|
||||
</DropdownContainer>
|
||||
)}
|
||||
</DropdownPromptContainer>
|
||||
</DropdownAndHeaderWrapper>
|
||||
|
||||
<FeeHeader>
|
||||
<Trans>Fees</Trans>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSellAsset } from 'nft/hooks'
|
||||
import { ListingWarning, WalletAsset } from 'nft/types'
|
||||
import { formatEth } from 'nft/utils/currency'
|
||||
import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { colors } from 'theme/colors'
|
||||
@@ -42,7 +43,11 @@ const GlobalPriceIcon = styled.div`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
`
|
||||
|
||||
const WarningMessage = styled(Row)<{ warningType: WarningType }>`
|
||||
const WarningRow = styled(Row)`
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
const WarningMessage = styled(Row)<{ $color: string }>`
|
||||
top: 52px;
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
@@ -50,17 +55,16 @@ const WarningMessage = styled(Row)<{ warningType: WarningType }>`
|
||||
font-weight: 600;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
color: ${({ warningType, theme }) => (warningType === WarningType.BELOW_FLOOR ? colors.red400 : theme.textSecondary)};
|
||||
color: ${({ $color }) => $color};
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
right: unset;
|
||||
}
|
||||
`
|
||||
|
||||
const WarningAction = styled.div<{ warningType: WarningType }>`
|
||||
margin-left: 8px;
|
||||
const WarningAction = styled.div`
|
||||
cursor: pointer;
|
||||
color: ${({ warningType, theme }) => (warningType === WarningType.BELOW_FLOOR ? theme.accentAction : colors.red400)};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
`
|
||||
|
||||
enum WarningType {
|
||||
@@ -73,10 +77,10 @@ const getWarningMessage = (warning: WarningType) => {
|
||||
let message = <></>
|
||||
switch (warning) {
|
||||
case WarningType.BELOW_FLOOR:
|
||||
message = <Trans>LISTING BELOW FLOOR </Trans>
|
||||
message = <Trans>below floor price.</Trans>
|
||||
break
|
||||
case WarningType.ALREADY_LISTED:
|
||||
message = <Trans>ALREADY LISTED FOR </Trans>
|
||||
message = <Trans>Already listed at</Trans>
|
||||
break
|
||||
}
|
||||
return message
|
||||
@@ -107,6 +111,7 @@ export const PriceTextInput = ({
|
||||
const [warningType, setWarningType] = useState(WarningType.NONE)
|
||||
const removeMarketplaceWarning = useSellAsset((state) => state.removeMarketplaceWarning)
|
||||
const removeSellAsset = useSellAsset((state) => state.removeSellAsset)
|
||||
const showResolveIssues = useSellAsset((state) => state.showResolveIssues)
|
||||
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||
const theme = useTheme()
|
||||
|
||||
@@ -121,9 +126,16 @@ export const PriceTextInput = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [listPrice])
|
||||
|
||||
const borderColor =
|
||||
warningType !== WarningType.NONE && !focused
|
||||
const percentBelowFloor = (1 - (listPrice ?? 0) / (asset.floorPrice ?? 0)) * 100
|
||||
|
||||
const warningColor =
|
||||
showResolveIssues && !listPrice
|
||||
? colors.red400
|
||||
: warningType !== WarningType.NONE && !focused
|
||||
? (warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20) ||
|
||||
warningType === WarningType.ALREADY_LISTED
|
||||
? colors.red400
|
||||
: theme.accentWarning
|
||||
: isGlobalPrice
|
||||
? theme.accentAction
|
||||
: listPrice != null
|
||||
@@ -132,7 +144,7 @@ export const PriceTextInput = ({
|
||||
|
||||
return (
|
||||
<PriceTextInputWrapper>
|
||||
<InputWrapper borderColor={borderColor}>
|
||||
<InputWrapper borderColor={warningColor}>
|
||||
<NumericInput
|
||||
as="input"
|
||||
pattern="[0-9]"
|
||||
@@ -164,27 +176,27 @@ export const PriceTextInput = ({
|
||||
</GlobalPriceIcon>
|
||||
)}
|
||||
</InputWrapper>
|
||||
<WarningMessage warningType={warningType}>
|
||||
<WarningMessage $color={warningColor}>
|
||||
{warning
|
||||
? warning.message
|
||||
: warningType !== WarningType.NONE && (
|
||||
<>
|
||||
{getWarningMessage(warningType)}
|
||||
|
||||
{warningType === WarningType.BELOW_FLOOR
|
||||
? formatEth(asset?.floorPrice ?? 0)
|
||||
: formatEth(asset?.floor_sell_order_price ?? 0)}
|
||||
ETH
|
||||
<WarningRow>
|
||||
<AlertTriangle height={16} width={16} color={warningColor} />
|
||||
<span>
|
||||
{warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `}
|
||||
{getWarningMessage(warningType)}
|
||||
|
||||
{warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`}
|
||||
</span>
|
||||
<WarningAction
|
||||
warningType={warningType}
|
||||
onClick={() => {
|
||||
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
|
||||
setWarningType(WarningType.NONE)
|
||||
}}
|
||||
>
|
||||
{warningType === WarningType.BELOW_FLOOR ? <Trans>DISMISS</Trans> : <Trans>REMOVE ITEM</Trans>}
|
||||
{warningType === WarningType.BELOW_FLOOR ? <Trans>Dismiss</Trans> : <Trans>Remove item</Trans>}
|
||||
</WarningAction>
|
||||
</>
|
||||
</WarningRow>
|
||||
)}
|
||||
</WarningMessage>
|
||||
</PriceTextInputWrapper>
|
||||
|
||||
@@ -79,10 +79,10 @@ const HeaderButtonWrap = styled(Row)`
|
||||
border-radius: 12px;
|
||||
width: 180px;
|
||||
justify-content: space-between;
|
||||
background: ${({ theme }) => theme.backgroundModule};
|
||||
background: ${({ theme }) => theme.backgroundInteractive};
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
@media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
width: 220px;
|
||||
@@ -124,7 +124,7 @@ const ModalWrapper = styled.div`
|
||||
|
||||
const DropdownWrapper = styled(Column)<{ isOpen: boolean }>`
|
||||
padding: 16px 0px;
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')};
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Plural } from '@lingui/macro'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import ms from 'ms.macro'
|
||||
import { SortDropdown } from 'nft/components/common/SortDropdown'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { NumericInput } from 'nft/components/layout/Input'
|
||||
import { bodySmall, buttonTextMedium, caption } from 'nft/css/common.css'
|
||||
import { bodySmall, caption } from 'nft/css/common.css'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { DropDownOption } from 'nft/types'
|
||||
import { pluralize } from 'nft/utils/roundAndPluralize'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
import { AlertTriangle, ChevronDown } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { Dropdown } from './Dropdown'
|
||||
|
||||
const ModalWrapper = styled(Column)`
|
||||
gap: 4px;
|
||||
@@ -25,17 +27,41 @@ const InputWrapper = styled(Row)<{ isInvalid: boolean }>`
|
||||
border-color: ${({ isInvalid, theme }) => (isInvalid ? theme.accentCritical : theme.backgroundOutline)};
|
||||
`
|
||||
|
||||
const DropdownWrapper = styled(ThemedText.BodyPrimary)`
|
||||
const DropdownPrompt = styled(Row)`
|
||||
gap: 4px;
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
height: min-content;
|
||||
width: 80px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
border-radius: 8px;
|
||||
padding: 6px 4px 6px 8px;
|
||||
width: min-content;
|
||||
white-space: nowrap;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
`
|
||||
|
||||
const DropdownChevron = styled(ChevronDown)<{ isOpen: boolean }>`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
transform: ${({ isOpen }) => isOpen && 'rotate(180deg)'};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `transform ${duration.fast} ${timing.ease}`};
|
||||
`
|
||||
|
||||
const DropdownContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
right: 0px;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
`
|
||||
|
||||
const ErrorMessage = styled(Row)`
|
||||
@@ -43,6 +69,7 @@ const ErrorMessage = styled(Row)`
|
||||
gap: 4px;
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const WarningIcon = styled(AlertTriangle)`
|
||||
@@ -65,39 +92,73 @@ enum ErrorState {
|
||||
|
||||
export const SetDurationModal = () => {
|
||||
const [duration, setDuration] = useState(Duration.day)
|
||||
const [displayDuration, setDisplayDuration] = useState(Duration.day)
|
||||
const [amount, setAmount] = useState('7')
|
||||
const [errorState, setErrorState] = useState(ErrorState.valid)
|
||||
const setGlobalExpiration = useSellAsset((state) => state.setGlobalExpiration)
|
||||
const [showDropdown, toggleShowDropdown] = useReducer((s) => !s, false)
|
||||
const durationDropdownRef = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(durationDropdownRef, showDropdown ? toggleShowDropdown : undefined)
|
||||
|
||||
const setCustomExpiration = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAmount(event.target.value.length ? event.target.value : '')
|
||||
setDuration(displayDuration)
|
||||
}
|
||||
const selectDuration = (duration: Duration) => {
|
||||
setDuration(duration)
|
||||
setDisplayDuration(duration)
|
||||
}
|
||||
|
||||
const durationOptions: DropDownOption[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
displayText: 'Hours',
|
||||
onClick: () => selectDuration(Duration.hour),
|
||||
displayText: 'hours',
|
||||
isSelected: duration === Duration.hour,
|
||||
onClick: () => {
|
||||
setDuration(Duration.hour)
|
||||
toggleShowDropdown()
|
||||
},
|
||||
},
|
||||
{
|
||||
displayText: 'Days',
|
||||
onClick: () => selectDuration(Duration.day),
|
||||
displayText: 'days',
|
||||
isSelected: duration === Duration.day,
|
||||
onClick: () => {
|
||||
setDuration(Duration.day)
|
||||
toggleShowDropdown()
|
||||
},
|
||||
},
|
||||
{
|
||||
displayText: 'Weeks',
|
||||
onClick: () => selectDuration(Duration.week),
|
||||
displayText: 'weeks',
|
||||
isSelected: duration === Duration.week,
|
||||
onClick: () => {
|
||||
setDuration(Duration.week)
|
||||
toggleShowDropdown()
|
||||
},
|
||||
},
|
||||
{
|
||||
displayText: 'Months',
|
||||
onClick: () => selectDuration(Duration.month),
|
||||
displayText: 'months',
|
||||
isSelected: duration === Duration.month,
|
||||
onClick: () => {
|
||||
setDuration(Duration.month)
|
||||
toggleShowDropdown()
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
[duration]
|
||||
)
|
||||
|
||||
let prompt
|
||||
switch (duration) {
|
||||
case Duration.hour:
|
||||
prompt = <Plural value={amount} _1="hour" other="hours" />
|
||||
break
|
||||
case Duration.day:
|
||||
prompt = <Plural value={amount} _1="day" other="days" />
|
||||
break
|
||||
case Duration.week:
|
||||
prompt = <Plural value={amount} _1="week" other="weeks" />
|
||||
break
|
||||
case Duration.month:
|
||||
prompt = <Plural value={amount} _1="month" other="months" />
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const expiration = convertDurationToExpiration(parseFloat(amount), duration)
|
||||
|
||||
@@ -108,7 +169,7 @@ export const SetDurationModal = () => {
|
||||
}, [amount, duration, setGlobalExpiration])
|
||||
|
||||
return (
|
||||
<ModalWrapper>
|
||||
<ModalWrapper ref={durationDropdownRef}>
|
||||
<InputWrapper isInvalid={errorState !== ErrorState.valid}>
|
||||
<NumericInput
|
||||
as="input"
|
||||
@@ -124,15 +185,14 @@ export const SetDurationModal = () => {
|
||||
onChange={setCustomExpiration}
|
||||
flexShrink="0"
|
||||
/>
|
||||
<DropdownWrapper className={buttonTextMedium}>
|
||||
<SortDropdown
|
||||
dropDownOptions={durationOptions}
|
||||
mini
|
||||
miniPrompt={displayDuration + (displayDuration === duration ? pluralize(parseFloat(amount)) : 's')}
|
||||
left={38}
|
||||
top={38}
|
||||
/>
|
||||
</DropdownWrapper>
|
||||
<DropdownPrompt onClick={toggleShowDropdown}>
|
||||
{prompt} <DropdownChevron isOpen={showDropdown} />
|
||||
</DropdownPrompt>
|
||||
{showDropdown && (
|
||||
<DropdownContainer>
|
||||
<Dropdown dropDownOptions={durationOptions} width={125} />
|
||||
</DropdownContainer>
|
||||
)}
|
||||
</InputWrapper>
|
||||
{errorState !== ErrorState.valid && (
|
||||
<ErrorMessage className={caption}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Row from 'components/Row'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
export const RemoveIconWrap = styled.div<{ hovered: boolean }>`
|
||||
@@ -8,3 +9,8 @@ export const RemoveIconWrap = styled.div<{ hovered: boolean }>`
|
||||
width: 32px;
|
||||
visibility: ${({ hovered }) => (hovered ? 'visible' : 'hidden')};
|
||||
`
|
||||
|
||||
export const TitleRow = styled(Row)`
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
|
||||
@@ -12,6 +12,12 @@ interface NFTListState {
|
||||
setListingStatus: (status: ListingStatus) => void
|
||||
setListings: (listings: ListingRow[]) => void
|
||||
setCollectionsRequiringApproval: (collections: CollectionRow[]) => void
|
||||
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void
|
||||
setCollectionStatusAndCallback: (
|
||||
collection: CollectionRow,
|
||||
status: ListingStatus,
|
||||
callback?: () => Promise<void>
|
||||
) => void
|
||||
}
|
||||
|
||||
export const useNFTList = create<NFTListState>()(
|
||||
@@ -34,19 +40,23 @@ export const useNFTList = create<NFTListState>()(
|
||||
setListings: (listings) =>
|
||||
set(() => {
|
||||
const updatedListings = listings.map((listing) => {
|
||||
const oldStatus = get().listings.find(
|
||||
const oldListing = get().listings.find(
|
||||
(oldListing) =>
|
||||
oldListing.asset.asset_contract.address === listing.asset.asset_contract.address &&
|
||||
oldListing.asset.tokenId === listing.asset.tokenId &&
|
||||
oldListing.marketplace.name === listing.marketplace.name &&
|
||||
oldListing.price === listing.price
|
||||
)?.status
|
||||
)
|
||||
const oldStatus = oldListing?.status
|
||||
const oldCallback = oldListing?.callback
|
||||
const status = () => {
|
||||
switch (oldStatus) {
|
||||
case ListingStatus.APPROVED:
|
||||
return ListingStatus.APPROVED
|
||||
case ListingStatus.FAILED:
|
||||
return listing.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.FAILED
|
||||
case ListingStatus.REJECTED:
|
||||
return listing.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.REJECTED
|
||||
default:
|
||||
return listing.status
|
||||
}
|
||||
@@ -54,6 +64,7 @@ export const useNFTList = create<NFTListState>()(
|
||||
return {
|
||||
...listing,
|
||||
status: status(),
|
||||
callback: oldCallback ?? listing.callback,
|
||||
}
|
||||
})
|
||||
return {
|
||||
@@ -62,7 +73,75 @@ export const useNFTList = create<NFTListState>()(
|
||||
}),
|
||||
setCollectionsRequiringApproval: (collections) =>
|
||||
set(() => {
|
||||
return { collectionsRequiringApproval: collections }
|
||||
const updatedCollections = collections.map((collection) => {
|
||||
const oldCollection = get().collectionsRequiringApproval.find(
|
||||
(oldCollection) =>
|
||||
oldCollection.collectionAddress === collection.collectionAddress &&
|
||||
oldCollection.marketplace.name === collection.marketplace.name
|
||||
)
|
||||
const oldStatus = oldCollection?.status
|
||||
const oldCallback = oldCollection?.callback
|
||||
const status = () => {
|
||||
switch (oldStatus) {
|
||||
case ListingStatus.APPROVED:
|
||||
return ListingStatus.APPROVED
|
||||
case ListingStatus.FAILED:
|
||||
return collection.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.FAILED
|
||||
case ListingStatus.REJECTED:
|
||||
return collection.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.REJECTED
|
||||
default:
|
||||
return collection.status
|
||||
}
|
||||
}
|
||||
return {
|
||||
...collection,
|
||||
status: status(),
|
||||
callback: oldCallback ?? collection.callback,
|
||||
}
|
||||
})
|
||||
return {
|
||||
collectionsRequiringApproval: updatedCollections,
|
||||
}
|
||||
}),
|
||||
setListingStatusAndCallback: (listing, status, callback) =>
|
||||
set(({ listings }) => {
|
||||
const listingsCopy = [...listings]
|
||||
const oldListingIndex = listingsCopy.findIndex(
|
||||
(oldListing) =>
|
||||
oldListing.name === listing.name &&
|
||||
oldListing.price === listing.price &&
|
||||
oldListing.marketplace.name === listing.marketplace.name
|
||||
)
|
||||
if (oldListingIndex > -1) {
|
||||
const updatedListing = {
|
||||
...listings[oldListingIndex],
|
||||
status,
|
||||
callback: callback ?? listings[oldListingIndex].callback,
|
||||
}
|
||||
listingsCopy.splice(oldListingIndex, 1, updatedListing)
|
||||
}
|
||||
return {
|
||||
listings: listingsCopy,
|
||||
}
|
||||
}),
|
||||
setCollectionStatusAndCallback: (collection, status, callback) =>
|
||||
set(({ collectionsRequiringApproval }) => {
|
||||
const collectionsCopy = [...collectionsRequiringApproval]
|
||||
const oldCollectionIndex = collectionsCopy.findIndex(
|
||||
(oldCollection) =>
|
||||
oldCollection.name === collection.name && oldCollection.marketplace.name === collection.marketplace.name
|
||||
)
|
||||
if (oldCollectionIndex > -1) {
|
||||
const updatedCollection = {
|
||||
...collectionsCopy[oldCollectionIndex],
|
||||
status,
|
||||
callback: callback ?? collectionsCopy[oldCollectionIndex].callback,
|
||||
}
|
||||
collectionsCopy.splice(oldCollectionIndex, 1, updatedCollection)
|
||||
}
|
||||
return {
|
||||
collectionsRequiringApproval: collectionsCopy,
|
||||
}
|
||||
}),
|
||||
}))
|
||||
)
|
||||
|
||||
29
src/nft/hooks/usePayWithAnyTokenSwap.ts
Normal file
29
src/nft/hooks/usePayWithAnyTokenSwap.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Currency, CurrencyAmount, NativeCurrency, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
|
||||
import { useBestTrade } from 'hooks/useBestTrade'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
|
||||
export default function usePayWithAnyTokenSwap(
|
||||
inputCurrency?: Currency,
|
||||
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
|
||||
): {
|
||||
state: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
maximumAmountIn: CurrencyAmount<Token> | undefined
|
||||
} {
|
||||
const { state, trade } = useBestTrade(TradeType.EXACT_OUTPUT, parsedOutputAmount, inputCurrency ?? undefined)
|
||||
const allowedSlippage = useAutoSlippageTolerance(trade)
|
||||
const maximumAmountIn = useMemo(() => {
|
||||
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
|
||||
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
|
||||
}, [allowedSlippage, trade])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
state,
|
||||
trade,
|
||||
maximumAmountIn,
|
||||
}
|
||||
}, [maximumAmountIn, state, trade])
|
||||
}
|
||||
48
src/nft/hooks/usePermit2Approval.ts
Normal file
48
src/nft/hooks/usePermit2Approval.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
export default function usePermit2Approval(
|
||||
amount?: CurrencyAmount<Token>,
|
||||
maximumAmount?: CurrencyAmount<Token>,
|
||||
enabled?: boolean
|
||||
) {
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const allowance = usePermit2Allowance(
|
||||
enabled ? maximumAmount ?? (amount?.currency.isToken ? (amount as CurrencyAmount<Token>) : undefined) : undefined,
|
||||
enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
|
||||
)
|
||||
const isApprovalLoading = allowance.state === AllowanceState.REQUIRED && allowance.isApprovalLoading
|
||||
const [isAllowancePending, setIsAllowancePending] = useState(false)
|
||||
const updateAllowance = useCallback(async () => {
|
||||
invariant(allowance.state === AllowanceState.REQUIRED)
|
||||
setIsAllowancePending(true)
|
||||
try {
|
||||
await allowance.approveAndPermit()
|
||||
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, {
|
||||
chain_id: chainId,
|
||||
token_symbol: maximumAmount?.currency.symbol,
|
||||
token_address: maximumAmount?.currency.address,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
setIsAllowancePending(false)
|
||||
}
|
||||
}, [allowance, chainId, maximumAmount?.currency.address, maximumAmount?.currency.symbol])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
allowance,
|
||||
isApprovalLoading,
|
||||
isAllowancePending,
|
||||
updateAllowance,
|
||||
}
|
||||
}, [allowance, isAllowancePending, isApprovalLoading, updateAllowance])
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { ListingMarket, ListingWarning, WalletAsset } from '../types'
|
||||
|
||||
interface SellAssetState {
|
||||
sellAssets: WalletAsset[]
|
||||
showResolveIssues: boolean
|
||||
selectSellAsset: (asset: WalletAsset) => void
|
||||
removeSellAsset: (asset: WalletAsset) => void
|
||||
reset: () => void
|
||||
@@ -12,15 +13,18 @@ interface SellAssetState {
|
||||
setAssetListPrice: (asset: WalletAsset, price?: number, marketplace?: ListingMarket) => void
|
||||
setGlobalMarketplaces: (marketplaces: ListingMarket[]) => void
|
||||
removeAssetMarketplace: (asset: WalletAsset, marketplace: ListingMarket) => void
|
||||
// TODO: After merging v2, see if this marketplace logic can be removed
|
||||
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
|
||||
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
|
||||
removeAllMarketplaceWarnings: () => void
|
||||
toggleShowResolveIssues: () => void
|
||||
}
|
||||
|
||||
export const useSellAsset = create<SellAssetState>()(
|
||||
devtools(
|
||||
(set) => ({
|
||||
sellAssets: [],
|
||||
showResolveIssues: false,
|
||||
selectSellAsset: (asset) =>
|
||||
set(({ sellAssets }) => {
|
||||
if (sellAssets.length === 0) return { sellAssets: [asset] }
|
||||
@@ -152,6 +156,11 @@ export const useSellAsset = create<SellAssetState>()(
|
||||
return { sellAssets: assetsCopy }
|
||||
})
|
||||
},
|
||||
toggleShowResolveIssues: () => {
|
||||
set(({ showResolveIssues }) => {
|
||||
return { showResolveIssues: !showResolveIssues }
|
||||
})
|
||||
},
|
||||
}),
|
||||
{ name: 'useSelectAsset' }
|
||||
)
|
||||
|
||||
@@ -14,14 +14,14 @@ import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
|
||||
import { LIST_PAGE_MARGIN } from './shared'
|
||||
import { LIST_PAGE_MARGIN, LIST_PAGE_MARGIN_MOBILE, LIST_PAGE_MARGIN_TABLET } from './shared'
|
||||
|
||||
const ProfilePageWrapper = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
scrollbar-width: none;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
height: auto;
|
||||
}
|
||||
`
|
||||
@@ -33,6 +33,16 @@ const LoadedAccountPage = styled.div<{ cartExpanded: boolean; isOnV2ListPage: bo
|
||||
isOnV2ListPage ? LIST_PAGE_MARGIN * 2 : cartExpanded ? XXXL_BAG_WIDTH : 0}px
|
||||
);
|
||||
margin: 0px ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN : 0)}px;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.lg}px) {
|
||||
width: calc(100% - ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_TABLET * 2 : 0)}px);
|
||||
margin: 0px ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_TABLET : 0)}px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
width: calc(100% - ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_MOBILE * 2 : 0)}px);
|
||||
margin: 0px ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_MOBILE : 0)}px;
|
||||
}
|
||||
`
|
||||
|
||||
const Center = styled.div`
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export const LIST_PAGE_MARGIN = 156
|
||||
export const LIST_PAGE_MARGIN_TABLET = 60
|
||||
export const LIST_PAGE_MARGIN_MOBILE = 16
|
||||
|
||||
@@ -171,6 +171,7 @@ export interface DropDownOption {
|
||||
reverseIndex?: number
|
||||
reverseOnClick?: () => void
|
||||
sortBy?: SortBy
|
||||
isSelected?: boolean
|
||||
}
|
||||
|
||||
export enum DetailsOrigin {
|
||||
|
||||
@@ -109,6 +109,7 @@ export interface ListingRow extends AssetRow {
|
||||
|
||||
export interface CollectionRow extends AssetRow {
|
||||
collectionAddress?: string
|
||||
isVerified?: boolean
|
||||
marketplace: ListingMarket
|
||||
}
|
||||
|
||||
@@ -117,8 +118,3 @@ export enum ProfilePageStateType {
|
||||
VIEWING,
|
||||
LISTING,
|
||||
}
|
||||
|
||||
export enum ListingResponse {
|
||||
TRY_AGAIN,
|
||||
SUCCESS,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DetailsOrigin, GenieAsset, UpdatedGenieAsset, WalletAsset } from 'nft/types'
|
||||
import { DetailsOrigin, GenieAsset, Listing, UpdatedGenieAsset, WalletAsset } from 'nft/types'
|
||||
|
||||
export function getRarityStatus(
|
||||
rarityStatusCache: Map<string, boolean>,
|
||||
@@ -44,3 +44,38 @@ export const generateTweetForPurchase = (assets: UpdatedGenieAsset[], txHashUrl:
|
||||
} with @Uniswap 🦄\n\nhttps://app.uniswap.org/#/nfts/collection/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769\n${txHashUrl}`
|
||||
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`
|
||||
}
|
||||
|
||||
function getMinListingPrice(listings: Listing[]): number {
|
||||
return Math.min(...listings.map((listing) => listing.price ?? 0)) ?? 0
|
||||
}
|
||||
|
||||
function mapAssetsToCollections(assets: WalletAsset[]): { collection: string; items: string[] }[] {
|
||||
const collections = assets.map((asset) => asset.collection?.twitterUrl ?? asset.collection?.name ?? '')
|
||||
const uniqueCollections = [...new Set(collections)]
|
||||
return uniqueCollections.map((collection) => {
|
||||
return {
|
||||
collection,
|
||||
items: assets
|
||||
.filter((asset) => asset.collection?.twitterUrl === collection || asset.collection?.name === collection)
|
||||
.map((asset) => asset.name ?? ''),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const generateTweetForList = (assets: WalletAsset[]): string => {
|
||||
const tweetText =
|
||||
assets.length == 1
|
||||
? `I just listed ${
|
||||
assets[0].collection?.twitterUrl
|
||||
? `${assets[0].collection?.twitterUrl} `
|
||||
: `${assets[0].collection?.name} ` ?? ''
|
||||
}${assets[0].name} for ${getMinListingPrice(assets[0].newListings ?? [])} ETH on ${assets[0].marketplaces
|
||||
?.map((market) => market.name)
|
||||
.join(', ')}. Buy it on @Uniswap at https://app.uniswap.org/#${getAssetHref(assets[0])}`
|
||||
: `I just listed ${
|
||||
assets.length
|
||||
} items on @Uniswap at https://app.uniswap.org/#/nfts/profile\n\nCollections: ${mapAssetsToCollections(assets)
|
||||
.map(({ collection, items }) => `${collection} ${items.map((item) => item).join(', ')}`)
|
||||
.join(', ')} \n\nMarketplaces: ${assets[0].marketplaces?.map((market) => market.name).join(', ')}`
|
||||
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function approveCollection(
|
||||
// setApprovalForAll() method
|
||||
const ERC721Contract = new Contract(collectionAddress, ERC721, signer)
|
||||
const signerAddress = await signer.getAddress()
|
||||
setStatus(ListingStatus.PENDING)
|
||||
|
||||
try {
|
||||
const approved = await ERC721Contract.isApprovedForAll(signerAddress, operator)
|
||||
if (approved) {
|
||||
@@ -160,8 +160,9 @@ export async function signListing(
|
||||
)
|
||||
|
||||
const order = await executeAllActions()
|
||||
setStatus(ListingStatus.PENDING)
|
||||
const res = await PostOpenSeaSellOrder(order)
|
||||
if (res) setStatus(ListingStatus.APPROVED)
|
||||
res ? setStatus(ListingStatus.APPROVED) : setStatus(ListingStatus.FAILED)
|
||||
return res
|
||||
} catch (error) {
|
||||
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
|
||||
@@ -228,7 +229,7 @@ export async function signListing(
|
||||
params: [],
|
||||
}
|
||||
const res = await createLooksRareOrder(payload)
|
||||
if (res) setStatus(ListingStatus.APPROVED)
|
||||
res ? setStatus(ListingStatus.APPROVED) : setStatus(ListingStatus.FAILED)
|
||||
return res
|
||||
} catch (error) {
|
||||
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
|
||||
@@ -262,7 +263,7 @@ export async function signListing(
|
||||
setStatus(ListingStatus.PENDING)
|
||||
// call server api
|
||||
const resp = await newX2Y2Order(payload)
|
||||
if (resp) setStatus(ListingStatus.APPROVED)
|
||||
resp ? setStatus(ListingStatus.APPROVED) : setStatus(ListingStatus.FAILED)
|
||||
return resp
|
||||
} catch (error) {
|
||||
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useAnalyticsReporter } from '../components/analytics'
|
||||
import ErrorBoundary from '../components/ErrorBoundary'
|
||||
import { PageTabs } from '../components/NavBar'
|
||||
import NavBar from '../components/NavBar'
|
||||
import Polling from '../components/Polling'
|
||||
import Popups from '../components/Popups'
|
||||
import { useIsExpertMode } from '../state/user/hooks'
|
||||
import DarkModeQueryParamReader from '../theme/components/DarkModeQueryParamReader'
|
||||
@@ -193,6 +194,7 @@ export default function App() {
|
||||
</HeaderWrapper>
|
||||
<BodyWrapper>
|
||||
<Popups />
|
||||
<Polling />
|
||||
<TopLevelModals />
|
||||
<Suspense fallback={<Loader />}>
|
||||
{isLoaded ? (
|
||||
|
||||
@@ -6,6 +6,7 @@ import Card, { CardType } from 'components/About/Card'
|
||||
import { MAIN_CARDS, MORE_CARDS } from 'components/About/constants'
|
||||
import ProtocolBanner from 'components/About/ProtocolBanner'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import { useSwapWidgetEnabled } from 'featureFlags/flags/swapWidget'
|
||||
import Swap from 'pages/Swap'
|
||||
import { parse } from 'qs'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
@@ -14,7 +15,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Link as NativeLink } from 'react-router-dom'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
@@ -255,23 +256,40 @@ const LandingSwapContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const LandingSwap = styled(Swap)`
|
||||
const SwapCss = css`
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme }) => theme.accentAction};
|
||||
transform: translateY(-4px);
|
||||
transition: ${({ theme }) => `transform ${theme.transition.duration.medium} ${theme.transition.timing.ease}`};
|
||||
}
|
||||
`
|
||||
|
||||
const LinkCss = css`
|
||||
text-decoration: none;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const LandingSwap = styled(Swap)`
|
||||
${SwapCss}
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme }) => theme.accentAction};
|
||||
}
|
||||
`
|
||||
|
||||
const Link = styled(NativeLink)`
|
||||
text-decoration: none;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
${LinkCss}
|
||||
`
|
||||
|
||||
const WidgetLandingLink = styled(NativeLink)`
|
||||
${LinkCss}
|
||||
${SwapCss}
|
||||
`
|
||||
|
||||
export default function Landing() {
|
||||
@@ -287,6 +305,8 @@ export default function Landing() {
|
||||
ignoreQueryPrefix: true,
|
||||
})
|
||||
|
||||
const swapWidgetEnabled = useSwapWidgetEnabled()
|
||||
|
||||
// This can be simplified significantly once the flag is removed! For now being explicit is clearer.
|
||||
useEffect(() => {
|
||||
if (queryParams.intro || !selectedWallet) {
|
||||
@@ -306,9 +326,15 @@ export default function Landing() {
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.LANDING_PAGE_SWAP_ELEMENT}
|
||||
>
|
||||
<Link to="/swap">
|
||||
<LandingSwap />
|
||||
</Link>
|
||||
{swapWidgetEnabled ? (
|
||||
<WidgetLandingLink to="/swap">
|
||||
<Swap />
|
||||
</WidgetLandingLink>
|
||||
) : (
|
||||
<Link to="/swap">
|
||||
<LandingSwap />
|
||||
</Link>
|
||||
)}
|
||||
</TraceEvent>
|
||||
</LandingSwapContainer>
|
||||
<Gradient isDarkMode={isDarkMode} />
|
||||
|
||||
@@ -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="/find?origin=/migrate/v2">
|
||||
<StyledInternalLink id="import-pool-link" to="/pool/v2/find">
|
||||
Import it.
|
||||
</StyledInternalLink>
|
||||
</Trans>
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function CTACards() {
|
||||
|
||||
return (
|
||||
<CTASection>
|
||||
<CTA1 href="https://support.uniswap.org/hc/en-us/articles/7423608592781">
|
||||
<CTA1 href="https://support.uniswap.org/hc/en-us/categories/8122334631437-Providing-Liquidity-">
|
||||
<ResponsiveColumn>
|
||||
<HeaderText>
|
||||
<Trans>Learn about providing liquidity</Trans> ↗
|
||||
|
||||
@@ -32,13 +32,14 @@ import { Link, useParams } from 'react-router-dom'
|
||||
import { Bound } from 'state/mint/v3/actions'
|
||||
import { useIsTransactionPending, useTransactionAdder } from 'state/transactions/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ExternalLink, HideExtraSmall, ThemedText } from 'theme'
|
||||
import { ExternalLink, HideExtraSmall, HideSmall, ThemedText } from 'theme'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { formatTickPrice } from 'utils/formatTickPrice'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import RangeBadge from '../../components/Badge/RangeBadge'
|
||||
import { SmallButtonPrimary } from '../../components/Button/index'
|
||||
import { getPriceOrderingFromPositionForUI } from '../../components/PositionListItem'
|
||||
import RateToggle from '../../components/RateToggle'
|
||||
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
|
||||
@@ -49,38 +50,20 @@ import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
|
||||
import { LoadingRows } from './styleds'
|
||||
|
||||
const PageWrapper = styled.div`
|
||||
padding: 68px 8px 0px;
|
||||
padding: 68px 16px 16px 16px;
|
||||
|
||||
min-width: 800px;
|
||||
max-width: 960px;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
padding: 48px 8px 0px;
|
||||
min-width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
padding-top: 20px;
|
||||
min-width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
min-width: 680px;
|
||||
max-width: 680px;
|
||||
`};
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
min-width: 600px;
|
||||
max-width: 600px;
|
||||
`};
|
||||
|
||||
@media only screen and (max-width: 620px) {
|
||||
min-width: 500px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
|
||||
min-width: 340px;
|
||||
max-width: 340px;
|
||||
`};
|
||||
`
|
||||
|
||||
const BadgeText = styled.div`
|
||||
@@ -120,23 +103,40 @@ const DoubleArrow = styled.span`
|
||||
margin: 0 1rem;
|
||||
`
|
||||
const ResponsiveRow = styled(RowBetween)`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
row-gap: 16px;
|
||||
width: 100%:
|
||||
`};
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
const ResponsiveButtonPrimary = styled(ButtonPrimary)`
|
||||
const ActionButtonResponsiveRow = styled(ResponsiveRow)`
|
||||
width: 50%;
|
||||
justify-content: flex-end;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
* {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ResponsiveButtonConfirmed = styled(ButtonConfirmed)`
|
||||
border-radius: 12px;
|
||||
padding: 6px 8px;
|
||||
width: fit-content;
|
||||
font-size: 16px;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
flex: 1 1 auto;
|
||||
width: 49%;
|
||||
`};
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
width: fit-content;
|
||||
}
|
||||
`
|
||||
|
||||
const NFTGrid = styled.div`
|
||||
@@ -624,13 +624,13 @@ export function PositionPage() {
|
||||
<RangeBadge removed={removed} inRange={inRange} />
|
||||
</RowFixed>
|
||||
{ownsNFT && (
|
||||
<RowFixed>
|
||||
<ActionButtonResponsiveRow>
|
||||
{currency0 && currency1 && feeAmount && tokenId ? (
|
||||
<ButtonGray
|
||||
as={Link}
|
||||
to={`/increase/${currencyId(currency0)}/${currencyId(currency1)}/${feeAmount}/${tokenId}`}
|
||||
width="fit-content"
|
||||
padding="6px 8px"
|
||||
width="fit-content"
|
||||
$borderRadius="12px"
|
||||
style={{ marginRight: '8px' }}
|
||||
>
|
||||
@@ -638,55 +638,58 @@ export function PositionPage() {
|
||||
</ButtonGray>
|
||||
) : null}
|
||||
{tokenId && !removed ? (
|
||||
<ResponsiveButtonPrimary
|
||||
<SmallButtonPrimary
|
||||
as={Link}
|
||||
to={`/remove/${tokenId}`}
|
||||
width="fit-content"
|
||||
padding="6px 8px"
|
||||
width="fit-content"
|
||||
$borderRadius="12px"
|
||||
>
|
||||
<Trans>Remove Liquidity</Trans>
|
||||
</ResponsiveButtonPrimary>
|
||||
</SmallButtonPrimary>
|
||||
) : null}
|
||||
</RowFixed>
|
||||
</ActionButtonResponsiveRow>
|
||||
)}
|
||||
</ResponsiveRow>
|
||||
<RowBetween></RowBetween>
|
||||
</AutoColumn>
|
||||
<ResponsiveRow align="flex-start">
|
||||
{'result' in metadata ? (
|
||||
<DarkCard
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-around',
|
||||
marginRight: '12px',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginRight: 12 }}>
|
||||
<HideSmall
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
}}
|
||||
>
|
||||
{'result' in metadata ? (
|
||||
<DarkCard
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-around',
|
||||
minWidth: '340px',
|
||||
}}
|
||||
>
|
||||
<NFT image={metadata.result.image} height={400} />
|
||||
</div>
|
||||
{typeof chainId === 'number' && owner && !ownsNFT ? (
|
||||
<ExternalLink href={getExplorerLink(chainId, owner, ExplorerDataType.ADDRESS)}>
|
||||
<Trans>Owner</Trans>
|
||||
</ExternalLink>
|
||||
) : null}
|
||||
</DarkCard>
|
||||
) : (
|
||||
<DarkCard
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
minWidth: '340px',
|
||||
}}
|
||||
>
|
||||
<Loader />
|
||||
</DarkCard>
|
||||
)}
|
||||
{typeof chainId === 'number' && owner && !ownsNFT ? (
|
||||
<ExternalLink href={getExplorerLink(chainId, owner, ExplorerDataType.ADDRESS)}>
|
||||
<Trans>Owner</Trans>
|
||||
</ExternalLink>
|
||||
) : null}
|
||||
</DarkCard>
|
||||
) : (
|
||||
<DarkCard
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
minWidth: '340px',
|
||||
}}
|
||||
>
|
||||
<Loader />
|
||||
</DarkCard>
|
||||
)}
|
||||
</HideSmall>
|
||||
<AutoColumn gap="sm" style={{ width: '100%', height: '100%' }}>
|
||||
<DarkCard>
|
||||
<AutoColumn gap="md" style={{ width: '100%' }}>
|
||||
@@ -714,7 +717,7 @@ export function PositionPage() {
|
||||
</ThemedText.DeprecatedMain>
|
||||
{typeof ratio === 'number' && !removed ? (
|
||||
<Badge style={{ marginLeft: '10px' }}>
|
||||
<ThemedText.DeprecatedMain fontSize={11}>
|
||||
<ThemedText.DeprecatedMain color={theme.textSecondary} fontSize={11}>
|
||||
<Trans>{inverted ? ratio : 100 - ratio}%</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</Badge>
|
||||
@@ -768,7 +771,7 @@ export function PositionPage() {
|
||||
</AutoColumn>
|
||||
{ownsNFT &&
|
||||
(feeValue0?.greaterThan(0) || feeValue1?.greaterThan(0) || !!collectMigrationHash) ? (
|
||||
<ButtonConfirmed
|
||||
<ResponsiveButtonConfirmed
|
||||
disabled={collecting || !!collectMigrationHash}
|
||||
confirmed={!!collectMigrationHash && !isCollectPending}
|
||||
width="fit-content"
|
||||
@@ -794,7 +797,7 @@ export function PositionPage() {
|
||||
</ThemedText.DeprecatedMain>
|
||||
</>
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
</ResponsiveButtonConfirmed>
|
||||
) : null}
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
|
||||
@@ -19,8 +19,10 @@ import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import Widget from 'components/Widget'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
|
||||
import { useSwapWidgetEnabled } from 'featureFlags/flags/swapWidget'
|
||||
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import { useSwapCallback } from 'hooks/useSwapCallback'
|
||||
import useTransactionDeadline from 'hooks/useTransactionDeadline'
|
||||
@@ -157,6 +159,7 @@ export default function Swap({ className }: { className?: string }) {
|
||||
const loadedUrlParams = useDefaultsFromURLSearch()
|
||||
const [newSwapQuoteNeedsLogging, setNewSwapQuoteNeedsLogging] = useState(true)
|
||||
const [fetchingSwapQuoteStartTime, setFetchingSwapQuoteStartTime] = useState<Date | undefined>()
|
||||
const swapWidgetEnabled = useSwapWidgetEnabled()
|
||||
|
||||
// token warning stuff
|
||||
const [loadedInputCurrency, loadedOutputCurrency] = [
|
||||
@@ -559,319 +562,335 @@ export default function Swap({ className }: { className?: string }) {
|
||||
showCancel={true}
|
||||
/>
|
||||
<PageWrapper>
|
||||
<SwapWrapper className={className} id="swap-page">
|
||||
<SwapHeader allowedSlippage={allowedSlippage} />
|
||||
<ConfirmSwapModal
|
||||
isOpen={showConfirm}
|
||||
trade={trade}
|
||||
originalTrade={tradeToConfirm}
|
||||
onAcceptChanges={handleAcceptChanges}
|
||||
attemptingTxn={attemptingTxn}
|
||||
txHash={txHash}
|
||||
recipient={recipient}
|
||||
allowedSlippage={allowedSlippage}
|
||||
onConfirm={handleSwap}
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
onDismiss={handleConfirmDismiss}
|
||||
swapQuoteReceivedDate={swapQuoteReceivedDate}
|
||||
fiatValueInput={fiatValueTradeInput}
|
||||
fiatValueOutput={fiatValueTradeOutput}
|
||||
{swapWidgetEnabled ? (
|
||||
<Widget
|
||||
defaultTokens={{
|
||||
[Field.INPUT]: loadedInputCurrency ?? undefined,
|
||||
[Field.OUTPUT]: loadedOutputCurrency ?? undefined,
|
||||
}}
|
||||
width="100%"
|
||||
/>
|
||||
) : (
|
||||
<SwapWrapper className={className} id="swap-page">
|
||||
<SwapHeader allowedSlippage={allowedSlippage} />
|
||||
<ConfirmSwapModal
|
||||
isOpen={showConfirm}
|
||||
trade={trade}
|
||||
originalTrade={tradeToConfirm}
|
||||
onAcceptChanges={handleAcceptChanges}
|
||||
attemptingTxn={attemptingTxn}
|
||||
txHash={txHash}
|
||||
recipient={recipient}
|
||||
allowedSlippage={allowedSlippage}
|
||||
onConfirm={handleSwap}
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
onDismiss={handleConfirmDismiss}
|
||||
swapQuoteReceivedDate={swapQuoteReceivedDate}
|
||||
fiatValueInput={fiatValueTradeInput}
|
||||
fiatValueOutput={fiatValueTradeOutput}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'relative' }}>
|
||||
<SwapSection>
|
||||
<Trace section={InterfaceSectionName.CURRENCY_INPUT_PANEL}>
|
||||
<SwapCurrencyInputPanel
|
||||
label={
|
||||
independentField === Field.OUTPUT && !showWrap ? (
|
||||
<Trans>From (at most)</Trans>
|
||||
) : (
|
||||
<Trans>From</Trans>
|
||||
)
|
||||
}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
showMaxButton={showMaxButton}
|
||||
currency={currencies[Field.INPUT] ?? null}
|
||||
onUserInput={handleTypeInput}
|
||||
onMax={handleMaxInput}
|
||||
fiatValue={fiatValueInput ?? undefined}
|
||||
onCurrencySelect={handleInputSelect}
|
||||
otherCurrency={currencies[Field.OUTPUT]}
|
||||
showCommonBases={true}
|
||||
id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
|
||||
loading={independentField === Field.OUTPUT && routeIsSyncing}
|
||||
/>
|
||||
</Trace>
|
||||
</SwapSection>
|
||||
<ArrowWrapper clickable={isSupportedChain(chainId)}>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SwapEventName.SWAP_TOKENS_REVERSED}
|
||||
element={InterfaceElementName.SWAP_TOKENS_REVERSE_ARROW_BUTTON}
|
||||
>
|
||||
<ArrowContainer
|
||||
onClick={() => {
|
||||
setApprovalSubmitted(false) // reset 2 step UI for approvals
|
||||
onSwitchTokens()
|
||||
}}
|
||||
color={theme.textPrimary}
|
||||
>
|
||||
<ArrowDown
|
||||
size="16"
|
||||
color={
|
||||
currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.textPrimary : theme.textTertiary
|
||||
}
|
||||
/>
|
||||
</ArrowContainer>
|
||||
</TraceEvent>
|
||||
</ArrowWrapper>
|
||||
</div>
|
||||
<AutoColumn gap="md">
|
||||
<div>
|
||||
<OutputSwapSection showDetailsDropdown={showDetailsDropdown}>
|
||||
<Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}>
|
||||
<div style={{ display: 'relative' }}>
|
||||
<SwapSection>
|
||||
<Trace section={InterfaceSectionName.CURRENCY_INPUT_PANEL}>
|
||||
<SwapCurrencyInputPanel
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={handleTypeOutput}
|
||||
label={
|
||||
independentField === Field.INPUT && !showWrap ? <Trans>To (at least)</Trans> : <Trans>To</Trans>
|
||||
independentField === Field.OUTPUT && !showWrap ? (
|
||||
<Trans>From (at most)</Trans>
|
||||
) : (
|
||||
<Trans>From</Trans>
|
||||
)
|
||||
}
|
||||
showMaxButton={false}
|
||||
hideBalance={false}
|
||||
fiatValue={fiatValueOutput ?? undefined}
|
||||
priceImpact={stablecoinPriceImpact}
|
||||
currency={currencies[Field.OUTPUT] ?? null}
|
||||
onCurrencySelect={handleOutputSelect}
|
||||
otherCurrency={currencies[Field.INPUT]}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
showMaxButton={showMaxButton}
|
||||
currency={currencies[Field.INPUT] ?? null}
|
||||
onUserInput={handleTypeInput}
|
||||
onMax={handleMaxInput}
|
||||
fiatValue={fiatValueInput ?? undefined}
|
||||
onCurrencySelect={handleInputSelect}
|
||||
otherCurrency={currencies[Field.OUTPUT]}
|
||||
showCommonBases={true}
|
||||
id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}
|
||||
loading={independentField === Field.INPUT && routeIsSyncing}
|
||||
id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
|
||||
loading={independentField === Field.OUTPUT && routeIsSyncing}
|
||||
/>
|
||||
</Trace>
|
||||
|
||||
{recipient !== null && !showWrap ? (
|
||||
<>
|
||||
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
|
||||
<ArrowWrapper clickable={false}>
|
||||
<ArrowDown size="16" color={theme.textSecondary} />
|
||||
</ArrowWrapper>
|
||||
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
|
||||
<Trans>- Remove recipient</Trans>
|
||||
</LinkStyledButton>
|
||||
</AutoRow>
|
||||
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
|
||||
</>
|
||||
) : null}
|
||||
</OutputSwapSection>
|
||||
{showDetailsDropdown && (
|
||||
<DetailsSwapSection>
|
||||
<SwapDetailsDropdown
|
||||
trade={trade}
|
||||
syncing={routeIsSyncing}
|
||||
loading={routeIsLoading}
|
||||
allowedSlippage={allowedSlippage}
|
||||
/>
|
||||
</DetailsSwapSection>
|
||||
)}
|
||||
</div>
|
||||
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
|
||||
<div>
|
||||
{swapIsUnsupported ? (
|
||||
<ButtonPrimary disabled={true}>
|
||||
<ThemedText.DeprecatedMain mb="4px">
|
||||
<Trans>Unsupported Asset</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</ButtonPrimary>
|
||||
) : !account ? (
|
||||
</SwapSection>
|
||||
<ArrowWrapper clickable={isSupportedChain(chainId)}>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
properties={{ received_swap_quote: getIsValidSwapQuote(trade, tradeState, swapInputError) }}
|
||||
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
|
||||
name={SwapEventName.SWAP_TOKENS_REVERSED}
|
||||
element={InterfaceElementName.SWAP_TOKENS_REVERSE_ARROW_BUTTON}
|
||||
>
|
||||
<ButtonLight onClick={toggleWalletModal} fontWeight={600}>
|
||||
<Trans>Connect Wallet</Trans>
|
||||
</ButtonLight>
|
||||
</TraceEvent>
|
||||
) : showWrap ? (
|
||||
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap} fontWeight={600}>
|
||||
{wrapInputError ? (
|
||||
<WrapErrorText wrapInputError={wrapInputError} />
|
||||
) : wrapType === WrapType.WRAP ? (
|
||||
<Trans>Wrap</Trans>
|
||||
) : wrapType === WrapType.UNWRAP ? (
|
||||
<Trans>Unwrap</Trans>
|
||||
) : null}
|
||||
</ButtonPrimary>
|
||||
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
|
||||
<GrayCard style={{ textAlign: 'center' }}>
|
||||
<ThemedText.DeprecatedMain mb="4px">
|
||||
<Trans>Insufficient liquidity for this trade.</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</GrayCard>
|
||||
) : showApproveFlow ? (
|
||||
<AutoRow style={{ flexWrap: 'nowrap', width: '100%' }}>
|
||||
<AutoColumn style={{ width: '100%' }} gap="12px">
|
||||
<ButtonConfirmed
|
||||
fontWeight={600}
|
||||
onClick={handleApprove}
|
||||
disabled={approveTokenButtonDisabled}
|
||||
width="100%"
|
||||
altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting
|
||||
confirmed={
|
||||
approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED
|
||||
<ArrowContainer
|
||||
onClick={() => {
|
||||
setApprovalSubmitted(false) // reset 2 step UI for approvals
|
||||
onSwitchTokens()
|
||||
}}
|
||||
color={theme.textPrimary}
|
||||
>
|
||||
<ArrowDown
|
||||
size="16"
|
||||
color={
|
||||
currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.textPrimary : theme.textTertiary
|
||||
}
|
||||
>
|
||||
<AutoRow justify="space-between" style={{ flexWrap: 'nowrap' }} height="20px">
|
||||
{/* we need to shorten this string on mobile */}
|
||||
{approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED ? (
|
||||
<ThemedText.SubHeader width="100%" textAlign="center" color="textSecondary">
|
||||
<Trans>You can now trade {currencies[Field.INPUT]?.symbol}</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
/>
|
||||
</ArrowContainer>
|
||||
</TraceEvent>
|
||||
</ArrowWrapper>
|
||||
</div>
|
||||
<AutoColumn gap="md">
|
||||
<div>
|
||||
<OutputSwapSection showDetailsDropdown={showDetailsDropdown}>
|
||||
<Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}>
|
||||
<SwapCurrencyInputPanel
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={handleTypeOutput}
|
||||
label={
|
||||
independentField === Field.INPUT && !showWrap ? (
|
||||
<Trans>To (at least)</Trans>
|
||||
) : (
|
||||
<ThemedText.SubHeader width="100%" textAlign="center" color="white">
|
||||
<Trans>Allow the Uniswap Protocol to use your {currencies[Field.INPUT]?.symbol}</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
)}
|
||||
<Trans>To</Trans>
|
||||
)
|
||||
}
|
||||
showMaxButton={false}
|
||||
hideBalance={false}
|
||||
fiatValue={fiatValueOutput ?? undefined}
|
||||
priceImpact={stablecoinPriceImpact}
|
||||
currency={currencies[Field.OUTPUT] ?? null}
|
||||
onCurrencySelect={handleOutputSelect}
|
||||
otherCurrency={currencies[Field.INPUT]}
|
||||
showCommonBases={true}
|
||||
id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}
|
||||
loading={independentField === Field.INPUT && routeIsSyncing}
|
||||
/>
|
||||
</Trace>
|
||||
|
||||
{approvalPending || approvalState === ApprovalState.PENDING ? (
|
||||
<Loader stroke={theme.white} />
|
||||
) : (approvalSubmitted && approvalState === ApprovalState.APPROVED) ||
|
||||
{recipient !== null && !showWrap ? (
|
||||
<>
|
||||
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
|
||||
<ArrowWrapper clickable={false}>
|
||||
<ArrowDown size="16" color={theme.textSecondary} />
|
||||
</ArrowWrapper>
|
||||
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
|
||||
<Trans>- Remove recipient</Trans>
|
||||
</LinkStyledButton>
|
||||
</AutoRow>
|
||||
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
|
||||
</>
|
||||
) : null}
|
||||
</OutputSwapSection>
|
||||
{showDetailsDropdown && (
|
||||
<DetailsSwapSection>
|
||||
<SwapDetailsDropdown
|
||||
trade={trade}
|
||||
syncing={routeIsSyncing}
|
||||
loading={routeIsLoading}
|
||||
allowedSlippage={allowedSlippage}
|
||||
/>
|
||||
</DetailsSwapSection>
|
||||
)}
|
||||
</div>
|
||||
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
|
||||
<div>
|
||||
{swapIsUnsupported ? (
|
||||
<ButtonPrimary disabled={true}>
|
||||
<ThemedText.DeprecatedMain mb="4px">
|
||||
<Trans>Unsupported Asset</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</ButtonPrimary>
|
||||
) : !account ? (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
properties={{ received_swap_quote: getIsValidSwapQuote(trade, tradeState, swapInputError) }}
|
||||
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
<ButtonLight onClick={toggleWalletModal} fontWeight={600}>
|
||||
<Trans>Connect Wallet</Trans>
|
||||
</ButtonLight>
|
||||
</TraceEvent>
|
||||
) : showWrap ? (
|
||||
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap} fontWeight={600}>
|
||||
{wrapInputError ? (
|
||||
<WrapErrorText wrapInputError={wrapInputError} />
|
||||
) : wrapType === WrapType.WRAP ? (
|
||||
<Trans>Wrap</Trans>
|
||||
) : wrapType === WrapType.UNWRAP ? (
|
||||
<Trans>Unwrap</Trans>
|
||||
) : null}
|
||||
</ButtonPrimary>
|
||||
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
|
||||
<GrayCard style={{ textAlign: 'center' }}>
|
||||
<ThemedText.DeprecatedMain mb="4px">
|
||||
<Trans>Insufficient liquidity for this trade.</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</GrayCard>
|
||||
) : showApproveFlow ? (
|
||||
<AutoRow style={{ flexWrap: 'nowrap', width: '100%' }}>
|
||||
<AutoColumn style={{ width: '100%' }} gap="12px">
|
||||
<ButtonConfirmed
|
||||
fontWeight={600}
|
||||
onClick={handleApprove}
|
||||
disabled={approveTokenButtonDisabled}
|
||||
width="100%"
|
||||
altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting
|
||||
confirmed={
|
||||
approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED
|
||||
}
|
||||
>
|
||||
<AutoRow justify="space-between" style={{ flexWrap: 'nowrap' }} height="20px">
|
||||
{/* we need to shorten this string on mobile */}
|
||||
{approvalState === ApprovalState.APPROVED ||
|
||||
signatureState === UseERC20PermitState.SIGNED ? (
|
||||
<CheckCircle size="20" color={theme.accentSuccess} />
|
||||
) : (
|
||||
<ThemedText.SubHeader width="100%" textAlign="center" color="textSecondary">
|
||||
<Trans>You can now trade {currencies[Field.INPUT]?.symbol}</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
) : (
|
||||
<ThemedText.SubHeader width="100%" textAlign="center" color="white">
|
||||
<Trans>Allow the Uniswap Protocol to use your {currencies[Field.INPUT]?.symbol}</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
)}
|
||||
|
||||
{approvalPending || approvalState === ApprovalState.PENDING ? (
|
||||
<Loader stroke={theme.white} />
|
||||
) : (approvalSubmitted && approvalState === ApprovalState.APPROVED) ||
|
||||
signatureState === UseERC20PermitState.SIGNED ? (
|
||||
<CheckCircle size="20" color={theme.accentSuccess} />
|
||||
) : (
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
You must give the Uniswap smart contracts permission to use your{' '}
|
||||
{currencies[Field.INPUT]?.symbol}. You only have to do this once per token.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<HelpCircle size="20" color={theme.white} style={{ marginLeft: '8px' }} />
|
||||
</MouseoverTooltip>
|
||||
)}
|
||||
</AutoRow>
|
||||
</ButtonConfirmed>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
if (isExpertMode) {
|
||||
handleSwap()
|
||||
} else {
|
||||
setSwapState({
|
||||
tradeToConfirm: trade,
|
||||
attemptingTxn: false,
|
||||
swapErrorMessage: undefined,
|
||||
showConfirm: true,
|
||||
txHash: undefined,
|
||||
})
|
||||
}
|
||||
}}
|
||||
width="100%"
|
||||
id="swap-button"
|
||||
disabled={
|
||||
!isValid ||
|
||||
routeIsSyncing ||
|
||||
routeIsLoading ||
|
||||
(approvalState !== ApprovalState.APPROVED &&
|
||||
signatureState !== UseERC20PermitState.SIGNED) ||
|
||||
priceImpactTooHigh
|
||||
}
|
||||
error={isValid && priceImpactSeverity > 2}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={600}>
|
||||
{priceImpactTooHigh ? (
|
||||
<Trans>High Price Impact</Trans>
|
||||
) : trade && priceImpactSeverity > 2 ? (
|
||||
<Trans>Swap Anyway</Trans>
|
||||
) : (
|
||||
<Trans>Swap</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
|
||||
<ButtonPrimary
|
||||
onClick={updateAllowance}
|
||||
disabled={isAllowancePending || isApprovalLoading}
|
||||
style={{ gap: 14 }}
|
||||
>
|
||||
{isAllowancePending ? (
|
||||
<>
|
||||
<Loader size="20px" />
|
||||
<Trans>Approve in your wallet</Trans>
|
||||
</>
|
||||
) : isApprovalLoading ? (
|
||||
<>
|
||||
<Loader size="20px" />
|
||||
<Trans>Approval pending</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ height: 20 }}>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
You must give the Uniswap smart contracts permission to use your{' '}
|
||||
{currencies[Field.INPUT]?.symbol}. You only have to do this once per token.
|
||||
Permission is required for Uniswap to swap each token. This will expire after one
|
||||
month for your security.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<HelpCircle size="20" color={theme.white} style={{ marginLeft: '8px' }} />
|
||||
<Info size={20} />
|
||||
</MouseoverTooltip>
|
||||
)}
|
||||
</AutoRow>
|
||||
</ButtonConfirmed>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
if (isExpertMode) {
|
||||
handleSwap()
|
||||
} else {
|
||||
setSwapState({
|
||||
tradeToConfirm: trade,
|
||||
attemptingTxn: false,
|
||||
swapErrorMessage: undefined,
|
||||
showConfirm: true,
|
||||
txHash: undefined,
|
||||
})
|
||||
}
|
||||
}}
|
||||
width="100%"
|
||||
id="swap-button"
|
||||
disabled={
|
||||
!isValid ||
|
||||
routeIsSyncing ||
|
||||
routeIsLoading ||
|
||||
(approvalState !== ApprovalState.APPROVED && signatureState !== UseERC20PermitState.SIGNED) ||
|
||||
priceImpactTooHigh
|
||||
}
|
||||
error={isValid && priceImpactSeverity > 2}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={600}>
|
||||
{priceImpactTooHigh ? (
|
||||
<Trans>High Price Impact</Trans>
|
||||
) : trade && priceImpactSeverity > 2 ? (
|
||||
<Trans>Swap Anyway</Trans>
|
||||
) : (
|
||||
<Trans>Swap</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
|
||||
<ButtonPrimary
|
||||
onClick={updateAllowance}
|
||||
disabled={isAllowancePending || isApprovalLoading}
|
||||
style={{ gap: 14 }}
|
||||
>
|
||||
{isAllowancePending ? (
|
||||
<>
|
||||
<Loader size="20px" />
|
||||
<Trans>Approve in your wallet</Trans>
|
||||
</>
|
||||
) : isApprovalLoading ? (
|
||||
<>
|
||||
<Loader size="20px" />
|
||||
<Trans>Approval pending</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ height: 20 }}>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
Permission is required for Uniswap to swap each token. This will expire after one month
|
||||
for your security.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<Info size={20} />
|
||||
</MouseoverTooltip>
|
||||
</div>
|
||||
<Trans>Approve use of {currencies[Field.INPUT]?.symbol}</Trans>
|
||||
</>
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
if (isExpertMode) {
|
||||
handleSwap()
|
||||
} else {
|
||||
setSwapState({
|
||||
tradeToConfirm: trade,
|
||||
attemptingTxn: false,
|
||||
swapErrorMessage: undefined,
|
||||
showConfirm: true,
|
||||
txHash: undefined,
|
||||
})
|
||||
}
|
||||
}}
|
||||
id="swap-button"
|
||||
disabled={
|
||||
!isValid ||
|
||||
routeIsSyncing ||
|
||||
routeIsLoading ||
|
||||
priceImpactTooHigh ||
|
||||
(permit2Enabled ? allowance.state !== AllowanceState.ALLOWED : Boolean(swapCallbackError))
|
||||
}
|
||||
error={
|
||||
isValid &&
|
||||
priceImpactSeverity > 2 &&
|
||||
(permit2Enabled ? allowance.state === AllowanceState.ALLOWED : !swapCallbackError)
|
||||
}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={600}>
|
||||
{swapInputError ? (
|
||||
swapInputError
|
||||
) : routeIsSyncing || routeIsLoading ? (
|
||||
<Trans>Swap</Trans>
|
||||
) : priceImpactTooHigh ? (
|
||||
<Trans>Price Impact Too High</Trans>
|
||||
) : priceImpactSeverity > 2 ? (
|
||||
<Trans>Swap Anyway</Trans>
|
||||
) : (
|
||||
<Trans>Swap</Trans>
|
||||
</div>
|
||||
<Trans>Approve use of {currencies[Field.INPUT]?.symbol}</Trans>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
)}
|
||||
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
</div>
|
||||
</AutoColumn>
|
||||
</SwapWrapper>
|
||||
</ButtonPrimary>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
if (isExpertMode) {
|
||||
handleSwap()
|
||||
} else {
|
||||
setSwapState({
|
||||
tradeToConfirm: trade,
|
||||
attemptingTxn: false,
|
||||
swapErrorMessage: undefined,
|
||||
showConfirm: true,
|
||||
txHash: undefined,
|
||||
})
|
||||
}
|
||||
}}
|
||||
id="swap-button"
|
||||
disabled={
|
||||
!isValid ||
|
||||
routeIsSyncing ||
|
||||
routeIsLoading ||
|
||||
priceImpactTooHigh ||
|
||||
(permit2Enabled ? allowance.state !== AllowanceState.ALLOWED : Boolean(swapCallbackError))
|
||||
}
|
||||
error={
|
||||
isValid &&
|
||||
priceImpactSeverity > 2 &&
|
||||
(permit2Enabled ? allowance.state === AllowanceState.ALLOWED : !swapCallbackError)
|
||||
}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={600}>
|
||||
{swapInputError ? (
|
||||
swapInputError
|
||||
) : routeIsSyncing || routeIsLoading ? (
|
||||
<Trans>Swap</Trans>
|
||||
) : priceImpactTooHigh ? (
|
||||
<Trans>Price Impact Too High</Trans>
|
||||
) : priceImpactSeverity > 2 ? (
|
||||
<Trans>Swap Anyway</Trans>
|
||||
) : (
|
||||
<Trans>Swap</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
)}
|
||||
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
</div>
|
||||
</AutoColumn>
|
||||
</SwapWrapper>
|
||||
)}
|
||||
<NetworkAlert />
|
||||
</PageWrapper>
|
||||
<SwitchLocaleLink />
|
||||
|
||||
@@ -1,51 +1,57 @@
|
||||
import TokenDetails from 'components/Tokens/TokenDetails'
|
||||
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { useTokenPriceQuery, useTokenQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
|
||||
import { TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
|
||||
|
||||
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
|
||||
|
||||
export default function TokenDetailsPage() {
|
||||
const { tokenAddress, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
|
||||
const { tokenAddress, chainName } = useParams<{ tokenAddress: string; chainName?: string }>()
|
||||
const chain = validateUrlChainParam(chainName)
|
||||
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
const isNative = tokenAddress === NATIVE_CHAIN_ID
|
||||
const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom)
|
||||
const [contract, duration] = useMemo(
|
||||
() => [
|
||||
{ address: isNative ? nativeOnChain(pageChainId).wrapped.address : tokenAddress ?? '', chain },
|
||||
toHistoryDuration(timePeriod),
|
||||
],
|
||||
[chain, isNative, pageChainId, timePeriod, tokenAddress]
|
||||
const [address, duration] = useMemo(
|
||||
/* tokenAddress will always be defined in the path for for this page to render, but useParams will always
|
||||
return optional arguments; nullish coalescing operator is present here to appease typechecker */
|
||||
() => [isNative ? getNativeTokenDBAddress(chain) : tokenAddress ?? '', toHistoryDuration(timePeriod)],
|
||||
[chain, isNative, timePeriod, tokenAddress]
|
||||
)
|
||||
|
||||
const { data: tokenQuery, loading: tokenQueryLoading } = useTokenQuery({
|
||||
const { data: tokenQuery } = useTokenQuery({
|
||||
variables: {
|
||||
contract: isNative ? { address: getNativeTokenDBAddress(chain), chain } : contract,
|
||||
address,
|
||||
chain,
|
||||
},
|
||||
})
|
||||
|
||||
const { data: tokenPriceQuery } = useTokenPriceQuery({
|
||||
variables: {
|
||||
contract,
|
||||
address,
|
||||
chain,
|
||||
duration,
|
||||
},
|
||||
})
|
||||
|
||||
if (!tokenQuery || tokenQueryLoading) return <TokenDetailsPageSkeleton />
|
||||
// Saves already-loaded chart data into state to display while tokenPriceQuery is undefined timePeriod input changes
|
||||
const [currentPriceQuery, setCurrentPriceQuery] = useState(tokenPriceQuery)
|
||||
useEffect(() => {
|
||||
if (tokenPriceQuery) setCurrentPriceQuery(tokenPriceQuery)
|
||||
}, [setCurrentPriceQuery, tokenPriceQuery])
|
||||
|
||||
if (!tokenQuery) return <TokenDetailsPageSkeleton />
|
||||
|
||||
return (
|
||||
<TokenDetails
|
||||
urlAddress={tokenAddress}
|
||||
chain={chain}
|
||||
tokenQuery={tokenQuery}
|
||||
tokenPriceQuery={tokenPriceQuery}
|
||||
tokenPriceQuery={currentPriceQuery}
|
||||
onChangeTimePeriod={setTimePeriod}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Trace } from '@uniswap/analytics'
|
||||
import { InterfacePageName } from '@uniswap/analytics-events'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { ButtonPrimary, SmallButtonPrimary } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { CardBGImage, CardNoise, CardSection, DataCard } from 'components/earn/styled'
|
||||
import FormattedCurrencyAmount from 'components/FormattedCurrencyAmount'
|
||||
@@ -106,12 +106,12 @@ const TextButton = styled(ThemedText.DeprecatedMain)`
|
||||
`
|
||||
|
||||
const AddressButton = styled.div`
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg3};
|
||||
padding: 2px 4px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
`
|
||||
|
||||
const StyledExternalLink = styled(ExternalLink)`
|
||||
@@ -223,14 +223,14 @@ export default function Landing() {
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<ButtonPrimary
|
||||
<SmallButtonPrimary
|
||||
as={Link}
|
||||
to="/create-proposal"
|
||||
style={{ width: 'fit-content', borderRadius: '8px' }}
|
||||
padding="8px"
|
||||
padding="6px 8px"
|
||||
>
|
||||
<Trans>Create Proposal</Trans>
|
||||
</ButtonPrimary>
|
||||
</SmallButtonPrimary>
|
||||
</AutoRow>
|
||||
</WrapSmall>
|
||||
{!showUnlockVoting && (
|
||||
@@ -263,6 +263,7 @@ export default function Landing() {
|
||||
|
||||
{allProposals?.length > 0 && (
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween></RowBetween>
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedMain>
|
||||
<Trans>Show Cancelled</Trans>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MockedProvider } from '@apollo/client/testing'
|
||||
import { i18n } from '@lingui/core'
|
||||
import { I18nProvider } from '@lingui/react'
|
||||
import { render, renderHook } from '@testing-library/react'
|
||||
@@ -32,9 +33,11 @@ const WithProviders = ({ children }: { children?: ReactNode }) => {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HashRouter>
|
||||
<Web3Provider>
|
||||
<BlockNumberProvider>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</BlockNumberProvider>
|
||||
<MockedProvider>
|
||||
<BlockNumberProvider>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</BlockNumberProvider>
|
||||
</MockedProvider>
|
||||
</Web3Provider>
|
||||
</HashRouter>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -182,6 +182,12 @@ const StyledLink = styled.a`
|
||||
${ClickableStyle}
|
||||
${LinkStyle}
|
||||
`
|
||||
|
||||
export const StyledRouterLink = styled(Link)`
|
||||
${ClickableStyle}
|
||||
${LinkStyle}
|
||||
`
|
||||
|
||||
/**
|
||||
* Outbound link that handles firing google analytics events
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@ import { nativeOnChain } from 'constants/tokens'
|
||||
import { Chain } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||
|
||||
export function getNativeTokenDBAddress(chain: Chain): string {
|
||||
export function getNativeTokenDBAddress(chain: Chain): string | undefined {
|
||||
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
switch (chain) {
|
||||
case Chain.Celo:
|
||||
@@ -12,6 +12,6 @@ export function getNativeTokenDBAddress(chain: Chain): string {
|
||||
case Chain.Arbitrum:
|
||||
case Chain.EthereumGoerli:
|
||||
case Chain.Optimism:
|
||||
return 'ETH'
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { _TypedDataEncoder } from '@ethersproject/hash'
|
||||
import { JsonRpcSigner } from '@ethersproject/providers'
|
||||
|
||||
/**
|
||||
* Overrides the _signTypedData method to add support for wallets without EIP-712 support (eg Zerion) by adding a fallback to eth_sign.
|
||||
* The implementation is copied from ethers (and linted), except for the catch statement, which removes the logger and adds the fallback.
|
||||
* @see https://github.com/ethers-io/ethers.js/blob/c80fcddf50a9023486e9f9acb1848aba4c19f7b6/packages/providers/src.ts/json-rpc-provider.ts#L334
|
||||
*/
|
||||
JsonRpcSigner.prototype._signTypedData = async function signTypedDataWithFallbacks(this, domain, types, value) {
|
||||
// Populate any ENS names (in-place)
|
||||
const populated = await _TypedDataEncoder.resolveNames(domain, types, value, (name: string) => {
|
||||
return this.provider.resolveName(name) as Promise<string>
|
||||
})
|
||||
|
||||
const address = await this.getAddress()
|
||||
|
||||
try {
|
||||
try {
|
||||
// We must try the unversioned eth_signTypedData first, because some wallets (eg SafePal) will hang on _v4.
|
||||
return await this.provider.send('eth_signTypedData', [
|
||||
address.toLowerCase(),
|
||||
JSON.stringify(_TypedDataEncoder.getPayload(populated.domain, types, populated.value)),
|
||||
])
|
||||
} catch (error) {
|
||||
// MetaMask complains that the unversioned eth_signTypedData is formatted incorrectly (32602) - it prefers _v4.
|
||||
if (error.code === -32602) {
|
||||
console.warn('eth_signTypedData failed, falling back to eth_signTypedData_v4:', error)
|
||||
return await this.provider.send('eth_signTypedData_v4', [
|
||||
address.toLowerCase(),
|
||||
JSON.stringify(_TypedDataEncoder.getPayload(populated.domain, types, populated.value)),
|
||||
])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
// If neither other method is available (eg Zerion), fallback to eth_sign.
|
||||
if (typeof error.message === 'string' && error.message.match(/not found/i)) {
|
||||
console.warn('eth_signTypedData_* failed, falling back to eth_sign:', error)
|
||||
const hash = _TypedDataEncoder.hash(populated.domain, types, populated.value)
|
||||
return await this.provider.send('eth_sign', [address, hash])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,21 @@ export function swapErrorToUserReadableMessage(error: any): string {
|
||||
error = error.error ?? error.data?.originalError
|
||||
}
|
||||
|
||||
// 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:
|
||||
if (
|
||||
// For Rainbow :
|
||||
(reason?.match(/request/i) && reason?.match(/reject/i)) ||
|
||||
// For Frame:
|
||||
reason?.match(/declined/i) ||
|
||||
// For SafePal:
|
||||
reason?.match(/cancelled by user/i) ||
|
||||
// For Coinbase:
|
||||
reason?.match(/user denied/i)
|
||||
) {
|
||||
return t`Transaction rejected`
|
||||
}
|
||||
|
||||
if (reason?.indexOf('execution reverted: ') === 0) reason = reason.substr('execution reverted: '.length)
|
||||
|
||||
switch (reason) {
|
||||
|
||||
47
yarn.lock
47
yarn.lock
@@ -4945,10 +4945,10 @@
|
||||
react "^18.2.0"
|
||||
react-dom "^18.2.0"
|
||||
|
||||
"@uniswap/conedison@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.2.1.tgz#c3dbfe14f4320fc5c60cde23c4bd70ed8a39c782"
|
||||
integrity sha512-ir6j7RQOyREXtW5YlmPjskfl7oDeHWtMFai57snThAkKgrb+8KTX5b0a5nbXeIuaW2RNHAaWTGoSoTneIHCAnQ==
|
||||
"@uniswap/conedison@^1.2.1", "@uniswap/conedison@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.3.0.tgz#998aca2bad27f0780a05b40e4512acfcadfece79"
|
||||
integrity sha512-zpZ52svBJ2btwl09mLOw7HlBxFDuYAjAZXLAR7WQZJeRgjD1yD2QuI3v7JliXvHzJh3ePYH6820EMp7xQbdAGQ==
|
||||
|
||||
"@uniswap/default-token-list@^2.0.0":
|
||||
version "2.2.0"
|
||||
@@ -5194,10 +5194,10 @@
|
||||
"@uniswap/v3-core" "1.0.0"
|
||||
"@uniswap/v3-periphery" "^1.0.1"
|
||||
|
||||
"@uniswap/widgets@^2.26.0":
|
||||
version "2.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.26.0.tgz#ed2d2b0965fc2139874a5eaca44c8070184d9e74"
|
||||
integrity sha512-LIfd3wgJxNiPXduyxXAihOzye5LrayyPDt0/OErSr1hF3BIiQqdVc3KyHDFJ4PC0pCp4+dfE2HPH1FrClO54Uw==
|
||||
"@uniswap/widgets@^2.27.0":
|
||||
version "2.27.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.27.0.tgz#debd946e6ff736a501a98321a2b8f98d7b055f22"
|
||||
integrity sha512-CHOoIORI0nYWN29s0ZrjGRVPiOWGy3hy8ZFhKM+CSz+mDNPWnlVssmDNBYhzM+tl2g/PcJpR49ggY+QWPlyDEQ==
|
||||
dependencies:
|
||||
"@babel/runtime" ">=7.17.0"
|
||||
"@fontsource/ibm-plex-mono" "^4.5.1"
|
||||
@@ -5743,10 +5743,10 @@
|
||||
dependencies:
|
||||
"@walletconnect/window-getters" "^1.0.0"
|
||||
|
||||
"@web3-react/coinbase-wallet@8.0.34-beta.0":
|
||||
version "8.0.34-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/coinbase-wallet/-/coinbase-wallet-8.0.34-beta.0.tgz#43d51bb440fb4b98cc2c33782714da30ab8e1fb6"
|
||||
integrity sha512-eGgtGtAqcRL64U1lcWeICB9CmpUycVl/mWD/b2Nd7yE0hXFUbPGLZHNvBTpnOpkANkjHI5ufFoyDGzzoxdw12A==
|
||||
"@web3-react/coinbase-wallet@8.0.35-beta.0":
|
||||
version "8.0.35-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/coinbase-wallet/-/coinbase-wallet-8.0.35-beta.0.tgz#903df113a0987f0b28c07941a82c05f227cbccf8"
|
||||
integrity sha512-wSITb75xIfERw1rZA8t35mJ3Lq459Emt2ybraWvT0TAIVTzHGCKvEm3WglEVWj5CMy1X1lrJvWJ0ZfWUj1/0Lg==
|
||||
dependencies:
|
||||
"@web3-react/types" "^8.0.20-beta.0"
|
||||
|
||||
@@ -5768,6 +5768,13 @@
|
||||
dependencies:
|
||||
"@web3-react/types" "^8.0.20-beta.0"
|
||||
|
||||
"@web3-react/eip1193@8.0.27-beta.0":
|
||||
version "8.0.27-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/eip1193/-/eip1193-8.0.27-beta.0.tgz#b768fc3ae6e234627e6a8ecd4d5c473c371a816b"
|
||||
integrity sha512-8aca1NDP+qigh/LOxSiC5N44qvVo1z5V7QI4T5iUpNrzbXEXV3G2JXutoW5fNEm+oK5fkHK64p9eZ6lo0qyMbQ==
|
||||
dependencies:
|
||||
"@web3-react/types" "^8.0.20-beta.0"
|
||||
|
||||
"@web3-react/empty@8.0.20-beta.0":
|
||||
version "8.0.20-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/empty/-/empty-8.0.20-beta.0.tgz#f8e2a6414ba49c7da3937776c213eb4c8ff6e2c7"
|
||||
@@ -5792,10 +5799,10 @@
|
||||
"@metamask/detect-provider" "^1.2.0"
|
||||
"@web3-react/types" "^8.0.20-beta.0"
|
||||
|
||||
"@web3-react/metamask@8.0.29-beta.0":
|
||||
version "8.0.29-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.29-beta.0.tgz#536536b8d4f22f21d3e109efaa8149939833f21b"
|
||||
integrity sha512-UPaVmNum6cJ/CwW5WYFMrm6GwiuY1hnuCYB+bV1Bs0xghdag2Laj8/mSfpFCsCHcvg1ZWTcr4bH+WyuYAHgUxw==
|
||||
"@web3-react/metamask@8.0.30-beta.0":
|
||||
version "8.0.30-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.30-beta.0.tgz#afab5d7cf556e3ec77836ff2b4753f6f301cfbeb"
|
||||
integrity sha512-SzL8/RUmLHEQCdd6KNjEIHXwuYiQrKX5e1Bgipvtm8MKBRk1ty17Aj7MKWNRcl/Qt06pLSxbcEJeT9kPIrc0mg==
|
||||
dependencies:
|
||||
"@metamask/detect-provider" "^1.2.0"
|
||||
"@web3-react/types" "^8.0.20-beta.0"
|
||||
@@ -5840,6 +5847,14 @@
|
||||
"@web3-react/types" "^8.0.20-beta.0"
|
||||
eventemitter3 "^4.0.7"
|
||||
|
||||
"@web3-react/walletconnect@8.0.37-beta.0":
|
||||
version "8.0.37-beta.0"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/walletconnect/-/walletconnect-8.0.37-beta.0.tgz#d375dc75f34e9bb225d26a88b16aa2a001b4e3bc"
|
||||
integrity sha512-BW8tKE0dlFWSLBC/AWKULZbS1axToN7kMREKVY3YK1uj8bMRsoWcOQPceQl21WuEyVXax2V8My8SJgWuxcumvw==
|
||||
dependencies:
|
||||
"@web3-react/types" "^8.0.20-beta.0"
|
||||
eventemitter3 "^4.0.7"
|
||||
|
||||
"@webassemblyjs/ast@1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
|
||||
|
||||
Reference in New Issue
Block a user