Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e64c0e88f | ||
|
|
fda28d9be3 | ||
|
|
bc92af6c15 | ||
|
|
9a257e0ca8 | ||
|
|
82646b77dd | ||
|
|
1992c5de06 | ||
|
|
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 | ||
|
|
302af21a22 | ||
|
|
b61a2d4111 | ||
|
|
9be26788a2 | ||
|
|
ed393de481 | ||
|
|
cf5c393d97 | ||
|
|
68d81a0040 | ||
|
|
53caa51ac3 | ||
|
|
409ba72f9f | ||
|
|
9d9b3dca78 | ||
|
|
a11c7e9573 | ||
|
|
31bbcae1ed | ||
|
|
a1f6c7270e |
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
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ module.exports = {
|
||||
jest: {
|
||||
configure(jestConfig) {
|
||||
return Object.assign({}, jestConfig, {
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format'],
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -10,14 +10,6 @@ describe('Universal search bar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should yield no results found when contract address is search term', () => {
|
||||
// Search for uni token contract address.
|
||||
cy.get('[data-cy="search-bar-input"]').last().type('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('[data-cy="search-bar"]')
|
||||
.should('contain.text', 'No tokens found.')
|
||||
.and('contain.text', 'No NFT collections found.')
|
||||
})
|
||||
|
||||
it('should yield clickable result for regular token or nft collection search term', () => {
|
||||
// Search for uni token by name.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
|
||||
@@ -64,7 +56,7 @@ describe('Universal search bar', () => {
|
||||
.should('be.eq', 3)
|
||||
})
|
||||
|
||||
it('should show blocked badge when blocked token is searched for', () => {
|
||||
it.skip('should show blocked badge when blocked token is searched for', () => {
|
||||
// Search for mTSLA, which is a blocked token.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
|
||||
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
|
||||
|
||||
14
package.json
14
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.1.1",
|
||||
"@uniswap/conedison": "^1.3.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
@@ -145,14 +145,14 @@
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/smart-order-router": "^2.10.0",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.30",
|
||||
"@uniswap/universal-router-sdk": "1.3.0",
|
||||
"@uniswap/universal-router-sdk": "1.3.4",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "2.25.1",
|
||||
"@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.28-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`
|
||||
@@ -120,7 +115,7 @@ const LogoSectionContent = () => {
|
||||
<>
|
||||
<StyledLogo src={isDarkMode ? darkUnicornImgSrc : lightUnicornImgSrc} alt="Uniswap Logo" />
|
||||
<SocialLinks>
|
||||
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
|
||||
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
|
||||
<DiscordIcon size={32} />
|
||||
</SocialLink>
|
||||
<TraceEvent
|
||||
@@ -132,7 +127,7 @@ const LogoSectionContent = () => {
|
||||
<TwitterIcon size={32} />
|
||||
</SocialLink>
|
||||
</TraceEvent>
|
||||
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
|
||||
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon size={32} />
|
||||
</SocialLink>
|
||||
</SocialLinks>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import {
|
||||
getConnection,
|
||||
getConnectionName,
|
||||
getHasCoinbaseExtensionInstalled,
|
||||
getHasMetaMaskExtensionInstalled,
|
||||
} from 'connection/utils'
|
||||
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMaskWallet } from 'connection/utils'
|
||||
import { useCallback } from 'react'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
@@ -215,8 +210,8 @@ export default function AccountDetails({
|
||||
const theme = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const hasMetaMaskExtension = getHasMetaMaskExtensionInstalled()
|
||||
const hasCoinbaseExtension = getHasCoinbaseExtensionInstalled()
|
||||
const hasMetaMaskExtension = getIsMetaMaskWallet()
|
||||
const hasCoinbaseExtension = getIsCoinbaseWallet()
|
||||
const isInjectedMobileBrowser = (hasMetaMaskExtension || hasCoinbaseExtension) && isMobile
|
||||
|
||||
function formatConnectorName() {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import { TokenQueryData } from 'graphql/data/Token'
|
||||
import { TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||
@@ -7,14 +9,14 @@ import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
|
||||
|
||||
export default function QueryTokenLogo(
|
||||
props: AssetLogoBaseProps & {
|
||||
token?: TopToken | TokenQueryData
|
||||
token?: TopToken | TokenQueryData | SearchToken
|
||||
}
|
||||
) {
|
||||
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
|
||||
|
||||
return (
|
||||
<AssetLogo
|
||||
isNative={props.token?.address === NATIVE_CHAIN_ID}
|
||||
isNative={props.token?.standard === TokenStandard.Native || props.token?.address === NATIVE_CHAIN_ID}
|
||||
chainId={chainId}
|
||||
address={props.token?.address}
|
||||
symbol={props.token?.symbol}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Modal from 'components/Modal'
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { CloseIcon, ThemedText } from 'theme'
|
||||
|
||||
import { useModalIsOpen, useToggleMetaMaskConnectionErrorModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
padding: 32px 32px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const ShortColumn = styled(AutoColumn)`
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
const InfoText = styled(Text)`
|
||||
padding: 0 12px 0 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const StyledButton = styled(ButtonPrimary)`
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const WarningIcon = styled(AlertTriangle)`
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 28px;
|
||||
stroke-width: 1px;
|
||||
margin-right: 4px;
|
||||
color: ${({ theme }) => theme.accentCritical};
|
||||
`
|
||||
|
||||
const onReconnect = () => window.location.reload()
|
||||
|
||||
export default function MetaMaskConnectionErrorModal() {
|
||||
const modalOpen = useModalIsOpen(ApplicationModal.METAMASK_CONNECTION_ERROR)
|
||||
const toggleModal = useToggleMetaMaskConnectionErrorModal()
|
||||
|
||||
return (
|
||||
<Modal isOpen={modalOpen} onDismiss={toggleModal} minHeight={false} maxHeight={90}>
|
||||
<Wrapper>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<div />
|
||||
<CloseIcon onClick={toggleModal} />
|
||||
</RowBetween>
|
||||
<Container>
|
||||
<AutoColumn>
|
||||
<LogoContainer>
|
||||
<WarningIcon />
|
||||
</LogoContainer>
|
||||
</AutoColumn>
|
||||
<ShortColumn>
|
||||
<InfoText>
|
||||
<ThemedText.HeadlineSmall marginBottom="8px">
|
||||
<Trans>Wallet disconnected</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ThemedText.BodySmall>
|
||||
<Trans>A MetaMask error caused your wallet to disconnect. Reload the page to reconnect.</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
</InfoText>
|
||||
</ShortColumn>
|
||||
<StyledButton onClick={onReconnect}>
|
||||
<Trans>Reload</Trans>
|
||||
</StyledButton>
|
||||
</Container>
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
102
src/components/NavBar/RecentlySearchedAssets.ts
Normal file
102
src/components/NavBar/RecentlySearchedAssets.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||
import { GenieCollection } from 'nft/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
|
||||
|
||||
type RecentlySearchedAsset = {
|
||||
isNft?: boolean
|
||||
address: string
|
||||
chain: Chain
|
||||
}
|
||||
|
||||
// Temporary measure used until backend supports addressing by "NATIVE"
|
||||
const NATIVE_QUERY_ADDRESS_INPUT = null as unknown as string
|
||||
function getQueryAddress(chain: Chain) {
|
||||
return getNativeTokenDBAddress(chain) ?? NATIVE_QUERY_ADDRESS_INPUT
|
||||
}
|
||||
|
||||
const recentlySearchedAssetsAtom = atomWithStorage<RecentlySearchedAsset[]>('recentlySearchedAssets', [])
|
||||
|
||||
export function useAddRecentlySearchedAsset() {
|
||||
const [searchHistory, updateSearchHistory] = useAtom(recentlySearchedAssetsAtom)
|
||||
|
||||
return useCallback(
|
||||
(asset: RecentlySearchedAsset) => {
|
||||
// Removes the new asset if it was already in the array
|
||||
const newHistory = searchHistory.filter(
|
||||
(oldAsset) => !(oldAsset.address === asset.address && oldAsset.chain === asset.chain)
|
||||
)
|
||||
newHistory.unshift(asset)
|
||||
updateSearchHistory(newHistory)
|
||||
},
|
||||
[searchHistory, updateSearchHistory]
|
||||
)
|
||||
}
|
||||
|
||||
export function useRecentlySearchedAssets() {
|
||||
const history = useAtomValue(recentlySearchedAssetsAtom)
|
||||
const shortenedHistory = useMemo(() => history.slice(0, 4), [history])
|
||||
|
||||
const { data: queryData, loading } = useRecentlySearchedAssetsQuery({
|
||||
variables: {
|
||||
collectionAddresses: shortenedHistory.filter((asset) => asset.isNft).map((asset) => asset.address),
|
||||
contracts: shortenedHistory
|
||||
.filter((asset) => !asset.isNft)
|
||||
.map((token) => ({
|
||||
address: token.address === NATIVE_CHAIN_ID ? getQueryAddress(token.chain) : token.address,
|
||||
chain: token.chain,
|
||||
})),
|
||||
},
|
||||
})
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (shortenedHistory.length === 0) return []
|
||||
else if (!queryData) return undefined
|
||||
// Collects both tokens and collections in a map, so they can later be returned in original order
|
||||
const resultsMap: { [key: string]: GenieCollection | SearchToken } = {}
|
||||
|
||||
const queryCollections = queryData?.nftCollections?.edges.map((edge) => edge.node as NonNullable<NftCollection>)
|
||||
const collections = queryCollections?.map(
|
||||
(queryCollection): GenieCollection => {
|
||||
return {
|
||||
address: queryCollection.nftContracts?.[0]?.address ?? '',
|
||||
isVerified: queryCollection?.isVerified,
|
||||
name: queryCollection?.name,
|
||||
stats: {
|
||||
floor_price: queryCollection?.markets?.[0]?.floorPrice?.value,
|
||||
total_supply: queryCollection?.numAssets,
|
||||
},
|
||||
imageUrl: queryCollection?.image?.url ?? '',
|
||||
}
|
||||
},
|
||||
[queryCollections]
|
||||
)
|
||||
collections?.forEach((collection) => (resultsMap[collection.address] = collection))
|
||||
queryData.tokens?.filter(Boolean).forEach((token) => {
|
||||
resultsMap[token.address ?? `NATIVE-${token.chain}`] = token
|
||||
})
|
||||
|
||||
const data: (SearchToken | GenieCollection)[] = []
|
||||
shortenedHistory.forEach((asset) => {
|
||||
if (asset.address === 'NATIVE') {
|
||||
// Handles special case where wMATIC data needs to be used for MATIC
|
||||
const native = nativeOnChain(CHAIN_NAME_TO_CHAIN_ID[asset.chain] ?? SupportedChainId.MAINNET)
|
||||
const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}`
|
||||
const result = resultsMap[queryAddress]
|
||||
if (result) data.push({ ...result, address: 'NATIVE', ...native })
|
||||
} else {
|
||||
const result = resultsMap[asset.address]
|
||||
if (result) data.push(result)
|
||||
}
|
||||
})
|
||||
return data
|
||||
}, [queryData, shortenedHistory])
|
||||
|
||||
return { data, loading }
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import clsx from 'clsx'
|
||||
import { useSearchTokens } from 'graphql/data/SearchTokens'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
@@ -12,7 +14,6 @@ import { Row } from 'nft/components/Flex'
|
||||
import { magicalGradientOnHover } from 'nft/css/common.css'
|
||||
import { useIsMobile, useIsTablet } from 'nft/hooks'
|
||||
import { fetchSearchCollections } from 'nft/queries'
|
||||
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@@ -64,16 +65,8 @@ export const SearchBar = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const { data: tokens, isLoading: tokensAreLoading } = useQuery(
|
||||
['searchTokens', debouncedSearchValue],
|
||||
() => fetchSearchTokens(debouncedSearchValue),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
enabled: !!debouncedSearchValue.length,
|
||||
}
|
||||
)
|
||||
const { chainId } = useWeb3React()
|
||||
const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1)
|
||||
|
||||
const isNFTPage = useIsNftPage()
|
||||
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import useTrendingTokens from 'graphql/data/TrendingTokens'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { subheadSmall } from 'nft/css/common.css'
|
||||
import { useSearchHistory } from 'nft/hooks'
|
||||
import { fetchTrendingCollections } from 'nft/queries'
|
||||
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
|
||||
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
|
||||
import { GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
|
||||
import { formatEthPrice } from 'nft/utils/currency'
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
|
||||
import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
|
||||
import * as styles from './SearchBar.css'
|
||||
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
|
||||
|
||||
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) {
|
||||
return (suggestion as FungibleToken).decimals === undefined
|
||||
function isCollection(suggestion: GenieCollection | SearchToken | TrendingCollection) {
|
||||
return (suggestion as SearchToken).decimals === undefined
|
||||
}
|
||||
|
||||
interface SearchBarDropdownSectionProps {
|
||||
toggleOpen: () => void
|
||||
suggestions: (GenieCollection | FungibleToken)[]
|
||||
suggestions: (GenieCollection | SearchToken)[]
|
||||
header: JSX.Element
|
||||
headerIcon?: JSX.Element
|
||||
hoveredIndex: number | undefined
|
||||
@@ -73,7 +76,7 @@ const SearchBarDropdownSection = ({
|
||||
) : (
|
||||
<TokenRow
|
||||
key={suggestion.address}
|
||||
token={suggestion as FungibleToken}
|
||||
token={suggestion as SearchToken}
|
||||
isHovered={hoveredIndex === index + startingIndex}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
@@ -92,9 +95,13 @@ const SearchBarDropdownSection = ({
|
||||
)
|
||||
}
|
||||
|
||||
function isKnownToken(token: SearchToken) {
|
||||
return token.project?.safetyLevel == SafetyLevel.Verified || token.project?.safetyLevel == SafetyLevel.MediumWarning
|
||||
}
|
||||
|
||||
interface SearchBarDropdownProps {
|
||||
toggleOpen: () => void
|
||||
tokens: FungibleToken[]
|
||||
tokens: SearchToken[]
|
||||
collections: GenieCollection[]
|
||||
queryText: string
|
||||
hasInput: boolean
|
||||
@@ -110,8 +117,10 @@ export const SearchBarDropdown = ({
|
||||
isLoading,
|
||||
}: SearchBarDropdownProps) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
|
||||
const { history: searchHistory, updateItem: updateSearchHistory } = useSearchHistory()
|
||||
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
|
||||
|
||||
const { data: searchHistory } = useRecentlySearchedAssets()
|
||||
const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array<SearchToken>(2)], [searchHistory])
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const isNFTPage = useIsNftPage()
|
||||
const isTokenPage = pathname.includes('/tokens')
|
||||
@@ -141,26 +150,12 @@ export const SearchBarDropdown = ({
|
||||
[isNFTPage, trendingCollectionResults]
|
||||
)
|
||||
|
||||
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
|
||||
['trendingTokens'],
|
||||
() => fetchTrendingTokens(4),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
useEffect(() => {
|
||||
trendingTokenResults?.forEach(updateSearchHistory)
|
||||
}, [trendingTokenResults, updateSearchHistory])
|
||||
const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)
|
||||
|
||||
const trendingTokensLength = isTokenPage ? 3 : 2
|
||||
const trendingTokens = useMemo(
|
||||
() =>
|
||||
trendingTokenResults
|
||||
? trendingTokenResults.slice(0, trendingTokensLength)
|
||||
: [...Array<FungibleToken>(trendingTokensLength)],
|
||||
[trendingTokenResults, trendingTokensLength]
|
||||
() => trendingTokenData?.slice(0, trendingTokensLength) ?? [...Array<SearchToken>(trendingTokensLength)],
|
||||
[trendingTokenData, trendingTokensLength]
|
||||
)
|
||||
|
||||
const totalSuggestions = hasInput
|
||||
@@ -197,10 +192,9 @@ export const SearchBarDropdown = ({
|
||||
}, [toggleOpen, hoveredIndex, totalSuggestions])
|
||||
|
||||
const hasVerifiedCollection = collections.some((collection) => collection.isVerified)
|
||||
const hasVerifiedToken = tokens.some((token) => token.onDefaultList)
|
||||
const hasKnownToken = tokens.some(isKnownToken)
|
||||
const showCollectionsFirst =
|
||||
(isNFTPage && (hasVerifiedCollection || !hasVerifiedToken)) ||
|
||||
(!isNFTPage && !hasVerifiedToken && hasVerifiedCollection)
|
||||
(isNFTPage && (hasVerifiedCollection || !hasKnownToken)) || (!isNFTPage && !hasKnownToken && hasVerifiedCollection)
|
||||
|
||||
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
|
||||
|
||||
@@ -277,6 +271,7 @@ export const SearchBarDropdown = ({
|
||||
}}
|
||||
header={<Trans>Recent searches</Trans>}
|
||||
headerIcon={<ClockIcon />}
|
||||
isLoading={!searchHistory}
|
||||
/>
|
||||
)}
|
||||
{!isNFTPage && (
|
||||
@@ -292,7 +287,7 @@ export const SearchBarDropdown = ({
|
||||
}}
|
||||
header={<Trans>Popular tokens</Trans>}
|
||||
headerIcon={<TrendingArrow />}
|
||||
isLoading={trendingTokensAreLoading}
|
||||
isLoading={!trendingTokenData}
|
||||
/>
|
||||
)}
|
||||
{!isTokenPage && (
|
||||
@@ -323,7 +318,7 @@ export const SearchBarDropdown = ({
|
||||
trendingCollections,
|
||||
trendingCollectionsAreLoading,
|
||||
trendingTokens,
|
||||
trendingTokensAreLoading,
|
||||
trendingTokenData,
|
||||
hoveredIndex,
|
||||
toggleOpen,
|
||||
shortenedHistory,
|
||||
@@ -334,6 +329,7 @@ export const SearchBarDropdown = ({
|
||||
queryText,
|
||||
totalSuggestions,
|
||||
trace,
|
||||
searchHistory,
|
||||
])
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
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 QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { checkSearchTokenWarning } from 'constants/tokenSafety'
|
||||
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import { getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { vars } from 'nft/css/sprinkles.css'
|
||||
import { useSearchHistory } from 'nft/hooks'
|
||||
import { FungibleToken, GenieCollection } from 'nft/types'
|
||||
import { GenieCollection } from 'nft/types'
|
||||
import { ethNumberStandardFormatter } from 'nft/utils/currency'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
@@ -23,11 +20,9 @@ import { Link, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
|
||||
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
|
||||
import * as styles from './SearchBar.css'
|
||||
|
||||
const StyledLogoContainer = styled(LogoContainer)`
|
||||
margin-right: 8px;
|
||||
`
|
||||
const PriceChangeContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -63,16 +58,15 @@ export const CollectionRow = ({
|
||||
}: CollectionRowProps) => {
|
||||
const [brokenImage, setBrokenImage] = useState(false)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const addToSearchHistory = useSearchHistory(
|
||||
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
|
||||
)
|
||||
|
||||
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(collection)
|
||||
addRecentlySearchedAsset({ ...collection, isNft: true, chain: Chain.Ethereum })
|
||||
toggleOpen()
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addToSearchHistory, collection, toggleOpen, eventProperties])
|
||||
}, [addRecentlySearchedAsset, collection, toggleOpen, eventProperties])
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
@@ -130,17 +124,8 @@ export const CollectionRow = ({
|
||||
)
|
||||
}
|
||||
|
||||
function useBridgedAddress(token: FungibleToken): [string | undefined, number | undefined, string | undefined] {
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const bridgedAddress = connectedChainId ? token.extensions?.bridgeInfo?.[connectedChainId]?.tokenAddress : undefined
|
||||
if (bridgedAddress && connectedChainId) {
|
||||
return [bridgedAddress, connectedChainId, getChainInfo(connectedChainId)?.circleLogoUrl]
|
||||
}
|
||||
return [undefined, undefined, undefined]
|
||||
}
|
||||
|
||||
interface TokenRowProps {
|
||||
token: FungibleToken
|
||||
token: SearchToken
|
||||
isHovered: boolean
|
||||
setHoveredIndex: (index: number | undefined) => void
|
||||
toggleOpen: () => void
|
||||
@@ -149,19 +134,18 @@ interface TokenRowProps {
|
||||
}
|
||||
|
||||
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
|
||||
const addToSearchHistory = useSearchHistory(
|
||||
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
|
||||
)
|
||||
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(token)
|
||||
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
|
||||
address && addRecentlySearchedAsset({ address, chain: token.chain })
|
||||
|
||||
toggleOpen()
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addToSearchHistory, toggleOpen, token, eventProperties])
|
||||
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
|
||||
|
||||
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
|
||||
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
|
||||
const tokenDetailsPath = getTokenDetailsURL(token)
|
||||
// Close the modal on escape
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
@@ -177,7 +161,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
}
|
||||
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
|
||||
|
||||
const arrow = getDeltaArrow(token.price24hChange, 18)
|
||||
const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 18)
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -190,37 +174,33 @@ 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>
|
||||
<QueryTokenLogo
|
||||
token={token}
|
||||
symbol={token.symbol}
|
||||
size="36px"
|
||||
backupImg={token.project?.logoUrl}
|
||||
style={{ paddingRight: '8px' }}
|
||||
/>
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{token.name}</Box>
|
||||
<TokenSafetyIcon warning={checkWarning(token.address)} />
|
||||
<TokenSafetyIcon warning={checkSearchTokenWarning(token)} />
|
||||
</Row>
|
||||
<Box className={styles.secondaryText}>{token.symbol}</Box>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Column className={styles.suggestionSecondaryContainer}>
|
||||
{token.priceUsd && (
|
||||
{token.market?.price?.value && (
|
||||
<Row gap="4">
|
||||
<Box className={styles.primaryText}>{formatUSDPrice(token.priceUsd)}</Box>
|
||||
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
|
||||
</Row>
|
||||
)}
|
||||
{token.price24hChange && (
|
||||
{token.market?.pricePercentChange?.value && (
|
||||
<PriceChangeContainer>
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
<PriceChangeText isNegative={token.price24hChange < 0}>
|
||||
{Math.abs(token.price24hChange).toFixed(2)}%
|
||||
<PriceChangeText isNegative={token.market.pricePercentChange.value < 0}>
|
||||
{Math.abs(token.market.pricePercentChange.value).toFixed(2)}%
|
||||
</PriceChangeText>
|
||||
</PriceChangeContainer>
|
||||
)}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import useGasPrice from 'hooks/useGasPrice'
|
||||
import { useIsLandingPage } from 'hooks/useIsLandingPage'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import useMachineTimeMs from 'hooks/useMachineTime'
|
||||
import JSBI from 'jsbi'
|
||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
@@ -71,17 +69,6 @@ const StyledPollingDot = styled.div<{ warning: boolean }>`
|
||||
transition: 250ms ease background-color;
|
||||
`
|
||||
|
||||
const StyledGasDot = styled.div`
|
||||
background-color: ${({ theme }) => theme.textTertiary};
|
||||
border-radius: 50%;
|
||||
height: 4px;
|
||||
min-height: 4px;
|
||||
min-width: 4px;
|
||||
position: relative;
|
||||
transition: 250ms ease background-color;
|
||||
width: 4px;
|
||||
`
|
||||
|
||||
const rotate360 = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
@@ -123,9 +110,6 @@ export default function Polling() {
|
||||
const isNftPage = useIsNftPage()
|
||||
const isLandingPage = useIsLandingPage()
|
||||
|
||||
const ethGasPrice = useGasPrice()
|
||||
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
|
||||
|
||||
const waitMsBeforeWarning =
|
||||
(chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
|
||||
|
||||
@@ -163,25 +147,6 @@ export default function Polling() {
|
||||
return (
|
||||
<RowFixed>
|
||||
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
|
||||
<ExternalLink href="https://etherscan.io/gastracker">
|
||||
{!!priceGwei && (
|
||||
<RowFixed style={{ marginRight: '8px' }}>
|
||||
<ThemedText.DeprecatedMain fontSize="11px" mr="8px">
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The current fast gas amount for sending a transaction on L1. Gas fees are paid in Ethereum's
|
||||
native currency Ether (ETH) and denominated in GWEI.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
{priceGwei.toString()} <Trans>gwei</Trans>
|
||||
</MouseoverTooltip>
|
||||
</ThemedText.DeprecatedMain>
|
||||
<StyledGasDot />
|
||||
</RowFixed>
|
||||
)}
|
||||
</ExternalLink>
|
||||
<StyledPollingBlockNumber breathe={isMounting} hovering={isHover} warning={warning}>
|
||||
<ExternalLink href={blockExternalLinkHref}>
|
||||
<MouseoverTooltip
|
||||
|
||||
@@ -99,9 +99,9 @@ export default function PositionList({
|
||||
</ToggleLabel>
|
||||
</ToggleWrap>
|
||||
</MobileHeader>
|
||||
{positions.map((p) => {
|
||||
return <PositionListItem key={p.tokenId.toString()} positionDetails={p} />
|
||||
})}
|
||||
{positions.map((p) => (
|
||||
<PositionListItem key={p.tokenId.toString()} {...p} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
38
src/components/PositionListItem/PositionListItem.test.tsx
Normal file
38
src/components/PositionListItem/PositionListItem.test.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { render, screen } from 'test-utils'
|
||||
|
||||
import PositionListItem from '.'
|
||||
|
||||
jest.mock('hooks/Tokens', () => {
|
||||
const originalModule = jest.requireActual('hooks/Tokens')
|
||||
const uniSDK = jest.requireActual('@uniswap/sdk-core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
useToken: jest.fn(
|
||||
() =>
|
||||
new uniSDK.Token(
|
||||
1,
|
||||
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
8,
|
||||
'https://www.example.com',
|
||||
'example.com coin'
|
||||
)
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when the name contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
render(<PositionListItem {...positionDetails} />)
|
||||
screen.debug()
|
||||
expect(screen.queryByText('.com', { exact: false })).toBe(null)
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent, Price, Token } from '@uniswap/sdk-core'
|
||||
import { Position } from '@uniswap/v3-sdk'
|
||||
@@ -15,9 +16,9 @@ import { Link } from 'react-router-dom'
|
||||
import { Bound } from 'state/mint/v3/actions'
|
||||
import styled from 'styled-components/macro'
|
||||
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
|
||||
import { PositionDetails } from 'types/position'
|
||||
import { formatTickPrice } from 'utils/formatTickPrice'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
import { hasURL } from 'utils/urlChecks'
|
||||
|
||||
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||
|
||||
@@ -109,7 +110,13 @@ const DataText = styled.div`
|
||||
`
|
||||
|
||||
interface PositionListItemProps {
|
||||
positionDetails: PositionDetails
|
||||
token0: string
|
||||
token1: string
|
||||
tokenId: BigNumber
|
||||
fee: number
|
||||
liquidity: BigNumber
|
||||
tickLower: number
|
||||
tickUpper: number
|
||||
}
|
||||
|
||||
export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||
@@ -166,16 +173,15 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||
}
|
||||
}
|
||||
|
||||
export default function PositionListItem({ positionDetails }: PositionListItemProps) {
|
||||
const {
|
||||
token0: token0Address,
|
||||
token1: token1Address,
|
||||
fee: feeAmount,
|
||||
liquidity,
|
||||
tickLower,
|
||||
tickUpper,
|
||||
} = positionDetails
|
||||
|
||||
export default function PositionListItem({
|
||||
token0: token0Address,
|
||||
token1: token1Address,
|
||||
tokenId,
|
||||
fee: feeAmount,
|
||||
liquidity,
|
||||
tickLower,
|
||||
tickUpper,
|
||||
}: PositionListItemProps) {
|
||||
const token0 = useToken(token0Address)
|
||||
const token1 = useToken(token1Address)
|
||||
|
||||
@@ -203,10 +209,23 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
|
||||
// check if price is within range
|
||||
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
|
||||
|
||||
const positionSummaryLink = '/pool/' + positionDetails.tokenId
|
||||
const positionSummaryLink = '/pool/' + tokenId
|
||||
|
||||
const removed = liquidity?.eq(0)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() =>
|
||||
[token0?.name, token0?.symbol, token1?.name, token1?.symbol].reduce(
|
||||
(acc, testString) => acc || Boolean(testString && hasURL(testString)),
|
||||
false
|
||||
),
|
||||
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
|
||||
)
|
||||
|
||||
if (containsURL) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkRow to={positionSummaryLink}>
|
||||
<RowBetween>
|
||||
|
||||
@@ -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,8 +32,10 @@ import { PaddedColumn, SearchInput, Separator } from './styleds'
|
||||
const ContentWrapper = styled(Column)`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1 1;
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
interface CurrencySearchProps {
|
||||
@@ -45,6 +47,7 @@ interface CurrencySearchProps {
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
onlyShowCurrenciesWithBalance?: boolean
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
@@ -56,6 +59,7 @@ export function CurrencySearch({
|
||||
disableNonToken,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
}: CurrencySearchProps) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
@@ -92,6 +96,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 +109,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 +130,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 +196,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) => {
|
||||
@@ -134,25 +133,27 @@ export default function TokenDetails({
|
||||
if (!address) return
|
||||
const bridgedAddress = crossChainMap[update]
|
||||
if (bridgedAddress) {
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL(bridgedAddress, update)))
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain })))
|
||||
} else if (didFetchFromChain || token?.isNative) {
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL(address, update)))
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
|
||||
}
|
||||
},
|
||||
[address, crossChainMap, didFetchFromChain, navigate, token?.isNative]
|
||||
[address, chain, crossChainMap, didFetchFromChain, navigate, token?.isNative]
|
||||
)
|
||||
useOnGlobalChainSwitch(navigateToTokenForChain)
|
||||
|
||||
const navigateToWidgetSelectedToken = useCallback(
|
||||
(token: Currency) => {
|
||||
const address = token.isNative ? NATIVE_CHAIN_ID : token.address
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL(address, chain)))
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
|
||||
},
|
||||
[chain, navigate]
|
||||
)
|
||||
|
||||
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;
|
||||
@@ -307,7 +289,9 @@ export const HEADER_DESCRIPTIONS: Record<TokenSortMethod, ReactNode | undefined>
|
||||
[TokenSortMethod.PRICE]: undefined,
|
||||
[TokenSortMethod.PERCENT_CHANGE]: undefined,
|
||||
[TokenSortMethod.TOTAL_VALUE_LOCKED]: (
|
||||
<Trans>Total value locked (TVL) is the amount of the asset that’s currently in a Uniswap v3 liquidity pool.</Trans>
|
||||
<Trans>
|
||||
Total value locked (TVL) is the aggregate amount of the asset available across all Uniswap v3 liquidity pools.
|
||||
</Trans>
|
||||
),
|
||||
[TokenSortMethod.VOLUME]: (
|
||||
<Trans>Volume is the amount of the asset that has been traded on Uniswap v3 during the selected time frame.</Trans>
|
||||
@@ -442,18 +426,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 +448,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,
|
||||
@@ -475,20 +458,17 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
return (
|
||||
<div ref={ref} data-testid={`token-table-row-${token.symbol}`}>
|
||||
<StyledLink
|
||||
to={getTokenDetailsURL(token.address ?? '', token.chain)}
|
||||
to={getTokenDetailsURL(token)}
|
||||
onClick={() =>
|
||||
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
|
||||
}
|
||||
>
|
||||
<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]}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ApplicationModal } from 'state/application/reducer'
|
||||
const Bag = lazy(() => import('nft/components/bag/Bag'))
|
||||
const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal'))
|
||||
const AirdropModal = lazy(() => import('components/AirdropModal'))
|
||||
const MetaMaskConnectionErrorModal = lazy(() => import('components/MetaMaskConnectionErrorModal'))
|
||||
|
||||
export default function TopLevelModals() {
|
||||
const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM)
|
||||
@@ -30,7 +29,6 @@ export default function TopLevelModals() {
|
||||
<Bag />
|
||||
<TransactionCompleteModal />
|
||||
<AirdropModal />
|
||||
<MetaMaskConnectionErrorModal />
|
||||
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -126,15 +126,6 @@ const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
|
||||
margin-left: 6px;
|
||||
width: 14px;
|
||||
`
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const StatusWrapper = styled.div`
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
width: 70%;
|
||||
`
|
||||
|
||||
const TruncatedTextStyle = css`
|
||||
text-overflow: ellipsis;
|
||||
@@ -142,8 +133,14 @@ const TruncatedTextStyle = css`
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const AccountNamesWrapper = styled.div`
|
||||
const FlexContainer = styled.div`
|
||||
${TruncatedTextStyle}
|
||||
padding-right: 4px;
|
||||
display: inline-flex;
|
||||
`
|
||||
|
||||
const AccountNamesWrapper = styled.div`
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
@@ -280,19 +277,17 @@ const AuthenticatedHeader = () => {
|
||||
return (
|
||||
<>
|
||||
<HeaderWrapper>
|
||||
<StatusWrapper>
|
||||
<FlexContainer>
|
||||
<StatusIcon connectionType={connectionType} size={24} />
|
||||
{ENSName ? (
|
||||
<AccountNamesWrapper>
|
||||
<ENSNameContainer>{ENSName}</ENSNameContainer>
|
||||
<AccountContainer>{account && shortenAddress(account, 2, 4)}</AccountContainer>
|
||||
</AccountNamesWrapper>
|
||||
) : (
|
||||
<ThemedText.SubHeader marginTop="2.5px">{account && shortenAddress(account, 2, 4)}</ThemedText.SubHeader>
|
||||
)}
|
||||
</FlexContainer>
|
||||
</StatusWrapper>
|
||||
<FlexContainer>
|
||||
<StatusIcon connectionType={connectionType} size={24} />
|
||||
{ENSName ? (
|
||||
<AccountNamesWrapper>
|
||||
<ENSNameContainer>{ENSName}</ENSNameContainer>
|
||||
<AccountContainer>{account && shortenAddress(account, 2, 4)}</AccountContainer>
|
||||
</AccountNamesWrapper>
|
||||
) : (
|
||||
<ThemedText.SubHeader marginTop="2.5px">{account && shortenAddress(account, 2, 4)}</ThemedText.SubHeader>
|
||||
)}
|
||||
</FlexContainer>
|
||||
<IconContainer>
|
||||
<IconButton onClick={copy} Icon={Copy}>
|
||||
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
|
||||
|
||||
@@ -57,7 +57,7 @@ const StyledChevron = styled(ChevronLeft)`
|
||||
const BackSection = styled.div`
|
||||
position: absolute;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
width: -webkit-fill-available;
|
||||
width: fill-available;
|
||||
margin: 0px 2vw 0px 0px;
|
||||
padding: 0px 0px 2vh 0px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
|
||||
@@ -22,9 +22,6 @@ jest.mock('.../../state/application/hooks', () => {
|
||||
useToggleWalletModal: () => {
|
||||
return
|
||||
},
|
||||
useToggleMetaMaskConnectionErrorModal: () => {
|
||||
return
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -73,8 +70,8 @@ it('loads Wallet Modal on desktop', async () => {
|
||||
|
||||
it('loads Wallet Modal on desktop with generic Injected', async () => {
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('Browser Wallet')).toBeInTheDocument()
|
||||
@@ -85,8 +82,8 @@ it('loads Wallet Modal on desktop with generic Injected', async () => {
|
||||
|
||||
it('loads Wallet Modal on desktop with MetaMask installed', async () => {
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('MetaMask')).toBeInTheDocument()
|
||||
@@ -99,8 +96,8 @@ it('loads Wallet Modal on mobile', async () => {
|
||||
UserAgentMock.isMobile = true
|
||||
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('Open in Coinbase Wallet')).toBeInTheDocument()
|
||||
@@ -112,8 +109,8 @@ it('loads Wallet Modal on MetaMask browser', async () => {
|
||||
UserAgentMock.isMobile = true
|
||||
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('MetaMask')).toBeInTheDocument()
|
||||
@@ -124,8 +121,8 @@ it('loads Wallet Modal on Coinbase Wallet browser', async () => {
|
||||
UserAgentMock.isMobile = true
|
||||
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(true)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('Coinbase Wallet')).toBeInTheDocument()
|
||||
|
||||
@@ -10,9 +10,9 @@ import { networkConnection } from 'connection'
|
||||
import {
|
||||
getConnection,
|
||||
getConnectionName,
|
||||
getHasCoinbaseExtensionInstalled,
|
||||
getHasMetaMaskExtensionInstalled,
|
||||
getIsCoinbaseWallet,
|
||||
getIsInjected,
|
||||
getIsMetaMaskWallet,
|
||||
} from 'connection/utils'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
@@ -253,8 +253,8 @@ export default function WalletModal({
|
||||
|
||||
function getOptions() {
|
||||
const isInjected = getIsInjected()
|
||||
const hasMetaMaskExtension = getHasMetaMaskExtensionInstalled()
|
||||
const hasCoinbaseExtension = getHasCoinbaseExtensionInstalled()
|
||||
const hasMetaMaskExtension = getIsMetaMaskWallet()
|
||||
const hasCoinbaseExtension = getIsCoinbaseWallet()
|
||||
|
||||
const isCoinbaseWalletBrowser = isMobile && hasCoinbaseExtension
|
||||
const isMetaMaskBrowser = isMobile && hasMetaMaskExtension
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { Connection } from 'connection'
|
||||
import { setMetMaskErrorHandler } from 'connection'
|
||||
import { getConnectionName } from 'connection/utils'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
@@ -9,19 +8,8 @@ import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/tra
|
||||
import useEagerlyConnect from 'hooks/useEagerlyConnect'
|
||||
import useOrderedConnections from 'hooks/useOrderedConnections'
|
||||
import { ReactNode, useEffect, useMemo } from 'react'
|
||||
import { useToggleMetaMaskConnectionErrorModal } from 'state/application/hooks'
|
||||
|
||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
// https://github.com/MetaMask/metamask-extension/issues/13375
|
||||
const toggleMetaMaskConnectionErrorModal = useToggleMetaMaskConnectionErrorModal()
|
||||
useEffect(() => {
|
||||
setMetMaskErrorHandler((error) => {
|
||||
if (error.code === 1013) {
|
||||
toggleMetaMaskConnectionErrorModal()
|
||||
}
|
||||
})
|
||||
}, [toggleMetaMaskConnectionErrorModal])
|
||||
|
||||
useEagerlyConnect()
|
||||
const connections = useOrderedConnections()
|
||||
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])
|
||||
|
||||
@@ -26,16 +26,17 @@ import {
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
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/'
|
||||
|
||||
@@ -44,19 +45,34 @@ 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()
|
||||
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const onConnectWalletClick = useCallback(() => {
|
||||
toggleWalletModal()
|
||||
return false // prevents the in-widget wallet modal from opening
|
||||
}, [toggleWalletModal])
|
||||
|
||||
const onSwitchChain = useCallback(
|
||||
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
|
||||
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
|
||||
@@ -152,8 +168,9 @@ 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}
|
||||
onSwitchChain={onSwitchChain}
|
||||
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
|
||||
@@ -172,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,5 @@
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
|
||||
import { RouterPreference, Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
|
||||
@@ -37,6 +37,8 @@ export function useSyncWidgetSettings() {
|
||||
[setAppSlippage]
|
||||
)
|
||||
|
||||
const [routerPreference, onRouterPreferenceChange] = useState(RouterPreference.API)
|
||||
|
||||
const onSettingsReset = useCallback(() => {
|
||||
setWidgetTtl(undefined)
|
||||
setAppTtl(DEFAULT_DEADLINE_FROM_NOW)
|
||||
@@ -46,11 +48,15 @@ export function useSyncWidgetSettings() {
|
||||
|
||||
const settings: SwapController['settings'] = useMemo(() => {
|
||||
const auto = appSlippage === 'auto'
|
||||
return { slippage: { auto, max: widgetSlippage }, transactionTtl: widgetTtl }
|
||||
}, [widgetSlippage, widgetTtl, appSlippage])
|
||||
return {
|
||||
slippage: { auto, max: widgetSlippage },
|
||||
transactionTtl: widgetTtl,
|
||||
routerPreference,
|
||||
}
|
||||
}, [appSlippage, widgetSlippage, widgetTtl, routerPreference])
|
||||
const settingsHandlers: SwapEventHandlers = useMemo(
|
||||
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange }),
|
||||
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange]
|
||||
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange }),
|
||||
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange]
|
||||
)
|
||||
|
||||
return { settings: { settings, ...settingsHandlers } }
|
||||
|
||||
@@ -1,45 +1,68 @@
|
||||
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',
|
||||
}
|
||||
|
||||
export const LIGHT_THEME = {
|
||||
export const LIGHT_THEME: Theme = {
|
||||
// surface
|
||||
container: lightTheme.backgroundSurface,
|
||||
interactive: lightTheme.backgroundInteractive,
|
||||
module: lightTheme.backgroundModule,
|
||||
accent: lightTheme.accentAction,
|
||||
dialog: lightTheme.backgroundBackdrop,
|
||||
accentSoft: lightTheme.accentActionSoft,
|
||||
container: lightTheme.backgroundSurface,
|
||||
module: lightTheme.backgroundModule,
|
||||
interactive: lightTheme.backgroundInteractive,
|
||||
outline: lightTheme.backgroundOutline,
|
||||
dialog: lightTheme.backgroundBackdrop,
|
||||
scrim: lightTheme.backgroundScrim,
|
||||
// text
|
||||
onAccent: lightTheme.white,
|
||||
primary: lightTheme.textPrimary,
|
||||
secondary: lightTheme.textSecondary,
|
||||
hint: lightTheme.textTertiary,
|
||||
onInteractive: lightTheme.accentTextDarkPrimary,
|
||||
// shadow
|
||||
deepShadow: lightTheme.deepShadow,
|
||||
networkDefaultShadow: lightTheme.networkDefaultShadow,
|
||||
|
||||
// state
|
||||
success: lightTheme.accentSuccess,
|
||||
warning: lightTheme.accentWarning,
|
||||
error: lightTheme.accentCritical,
|
||||
|
||||
...fonts,
|
||||
zIndex,
|
||||
}
|
||||
|
||||
export const DARK_THEME = {
|
||||
export const DARK_THEME: Theme = {
|
||||
// surface
|
||||
container: darkTheme.backgroundSurface,
|
||||
interactive: darkTheme.backgroundInteractive,
|
||||
module: darkTheme.backgroundModule,
|
||||
accent: darkTheme.accentAction,
|
||||
dialog: darkTheme.backgroundBackdrop,
|
||||
accentSoft: darkTheme.accentActionSoft,
|
||||
container: darkTheme.backgroundSurface,
|
||||
module: darkTheme.backgroundModule,
|
||||
interactive: darkTheme.backgroundInteractive,
|
||||
outline: darkTheme.backgroundOutline,
|
||||
dialog: darkTheme.backgroundBackdrop,
|
||||
scrim: darkTheme.backgroundScrim,
|
||||
// text
|
||||
onAccent: darkTheme.white,
|
||||
primary: darkTheme.textPrimary,
|
||||
secondary: darkTheme.textSecondary,
|
||||
hint: darkTheme.textTertiary,
|
||||
onInteractive: darkTheme.accentTextLightPrimary,
|
||||
// shadow
|
||||
deepShadow: darkTheme.deepShadow,
|
||||
networkDefaultShadow: darkTheme.networkDefaultShadow,
|
||||
// state
|
||||
success: darkTheme.accentSuccess,
|
||||
warning: darkTheme.accentWarning,
|
||||
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 } }
|
||||
}
|
||||
|
||||
@@ -25,22 +25,10 @@ export interface Connection {
|
||||
type: ConnectionType
|
||||
}
|
||||
|
||||
type MetaMaskError = Error & { code: number }
|
||||
|
||||
let metaMaskErrorHandler: (error: MetaMaskError) => void | undefined
|
||||
export function setMetMaskErrorHandler(errorHandler: typeof metaMaskErrorHandler) {
|
||||
metaMaskErrorHandler = errorHandler
|
||||
}
|
||||
|
||||
function onError(error: Error) {
|
||||
console.debug(`web3-react error: ${error}`)
|
||||
}
|
||||
|
||||
function onMetaMaskError(error: Error) {
|
||||
onError(error)
|
||||
metaMaskErrorHandler?.(error as MetaMaskError)
|
||||
}
|
||||
|
||||
const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
|
||||
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
|
||||
)
|
||||
@@ -50,9 +38,7 @@ export const networkConnection: Connection = {
|
||||
type: ConnectionType.NETWORK,
|
||||
}
|
||||
|
||||
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>(
|
||||
(actions) => new MetaMask({ actions, onError: onMetaMaskError })
|
||||
)
|
||||
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
|
||||
export const injectedConnection: Connection = {
|
||||
connector: web3Injected,
|
||||
hooks: web3InjectedHooks,
|
||||
|
||||
@@ -12,18 +12,21 @@ export function getIsInjected(): boolean {
|
||||
return Boolean(window.ethereum)
|
||||
}
|
||||
|
||||
export function getHasMetaMaskExtensionInstalled(): boolean {
|
||||
return window.ethereum?.isMetaMask ?? false
|
||||
export function getIsBraveWallet(): boolean {
|
||||
return window.ethereum?.isBraveWallet ?? false
|
||||
}
|
||||
|
||||
export function getHasCoinbaseExtensionInstalled(): boolean {
|
||||
export function getIsMetaMaskWallet(): boolean {
|
||||
// When using Brave browser, `isMetaMask` is set to true when using the built-in wallet
|
||||
// This function should return true only when using the MetaMask extension
|
||||
// https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask
|
||||
return (window.ethereum?.isMetaMask ?? false) && !getIsBraveWallet()
|
||||
}
|
||||
|
||||
export function getIsCoinbaseWallet(): boolean {
|
||||
return window.ethereum?.isCoinbaseWallet ?? false
|
||||
}
|
||||
|
||||
export function getIsMetaMask(connectionType: ConnectionType): boolean {
|
||||
return connectionType === ConnectionType.INJECTED && getHasMetaMaskExtensionInstalled()
|
||||
}
|
||||
|
||||
const CONNECTIONS = [
|
||||
gnosisSafeConnection,
|
||||
injectedConnection,
|
||||
@@ -56,7 +59,7 @@ export function getConnection(c: Connector | ConnectionType) {
|
||||
|
||||
export function getConnectionName(
|
||||
connectionType: ConnectionType,
|
||||
hasMetaMaskExtension: boolean = getHasMetaMaskExtensionInstalled()
|
||||
hasMetaMaskExtension: boolean = getIsMetaMaskWallet()
|
||||
) {
|
||||
switch (connectionType) {
|
||||
case ConnectionType.INJECTED:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
|
||||
import { ZERO_ADDRESS } from './misc'
|
||||
import { NATIVE_CHAIN_ID } from './tokens'
|
||||
@@ -94,3 +96,11 @@ export function checkWarning(tokenAddress: string) {
|
||||
return BlockedWarning
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(cartcrom): Replace all usage of WARNING_LEVEL with SafetyLevel
|
||||
export function checkSearchTokenWarning(token: SearchToken) {
|
||||
if (!token.address) {
|
||||
return token.standard === TokenStandard.Native ? null : StrongWarning
|
||||
}
|
||||
return checkWarning(token.address)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
59
src/graphql/data/RecentlySearched.ts
Normal file
59
src/graphql/data/RecentlySearched.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
gql`
|
||||
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
|
||||
nftCollections(filter: { addresses: $collectionAddresses }) {
|
||||
edges {
|
||||
node {
|
||||
collectionId
|
||||
image {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
name
|
||||
numAssets
|
||||
nftContracts {
|
||||
address
|
||||
}
|
||||
markets(currencies: ETH) {
|
||||
floorPrice {
|
||||
currency
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens(contracts: $contracts) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
102
src/graphql/data/SearchTokens.ts
Normal file
102
src/graphql/data/SearchTokens.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import gql from 'graphql-tag'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { Chain, SearchTokensQuery, useSearchTokensQuery } from './__generated__/types-and-hooks'
|
||||
import { chainIdToBackendName } from './util'
|
||||
|
||||
gql`
|
||||
query SearchTokens($searchQuery: String!) {
|
||||
searchTokens(searchQuery: $searchQuery) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export type SearchToken = NonNullable<NonNullable<SearchTokensQuery['searchTokens']>[number]>
|
||||
|
||||
function isMoreRevelantToken(current: SearchToken, existing: SearchToken | undefined, searchChain: Chain) {
|
||||
if (!existing) return true
|
||||
|
||||
// Always priotize natives, and if both tokens are native, prefer native on current chain (i.e. Matic on Polygon over Matic on Mainnet )
|
||||
if (current.standard === 'NATIVE' && (existing.standard !== 'NATIVE' || current.chain === searchChain)) return true
|
||||
|
||||
// Prefer tokens on the searched chain, otherwise prefer mainnet tokens
|
||||
return current.chain === searchChain || (existing.chain !== searchChain && current.chain === Chain.Ethereum)
|
||||
}
|
||||
|
||||
// Places natives first, wrapped native on current chain next, then sorts by volume
|
||||
function searchTokenSortFunction(
|
||||
searchChain: Chain,
|
||||
wrappedNativeAddress: string | undefined,
|
||||
a: SearchToken,
|
||||
b: SearchToken
|
||||
) {
|
||||
if (a.standard === 'NATIVE') {
|
||||
if (b.standard === 'NATIVE') {
|
||||
if (a.chain === searchChain) return -1
|
||||
else if (b.chain === searchChain) return 1
|
||||
else return 0
|
||||
} else return -1
|
||||
} else if (b.standard === 'NATIVE') return 1
|
||||
else if (wrappedNativeAddress && a.address === wrappedNativeAddress) return -1
|
||||
else if (wrappedNativeAddress && b.address === wrappedNativeAddress) return 1
|
||||
else return (b.market?.volume24H?.value ?? 0) - (a.market?.volume24H?.value ?? 0)
|
||||
}
|
||||
|
||||
export function useSearchTokens(searchQuery: string, chainId: number) {
|
||||
const { data, loading, error } = useSearchTokensQuery({
|
||||
variables: {
|
||||
searchQuery,
|
||||
},
|
||||
})
|
||||
|
||||
const sortedTokens = useMemo(() => {
|
||||
const searchChain = chainIdToBackendName(chainId)
|
||||
// Stores results, allowing overwriting cross-chain tokens w/ more 'relevant token'
|
||||
const selectionMap: { [projectId: string]: SearchToken } = {}
|
||||
data?.searchTokens?.forEach((token) => {
|
||||
if (token.project?.id) {
|
||||
const existing = selectionMap[token.project.id]
|
||||
if (isMoreRevelantToken(token, existing, searchChain)) selectionMap[token.project.id] = token
|
||||
}
|
||||
})
|
||||
return Object.values(selectionMap).sort(
|
||||
searchTokenSortFunction.bind(null, searchChain, WRAPPED_NATIVE_CURRENCY[chainId]?.address)
|
||||
)
|
||||
}, [data, chainId])
|
||||
|
||||
return {
|
||||
data: sortedTokens,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
@@ -14,40 +14,49 @@ 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
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
standard
|
||||
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 +67,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!) {
|
||||
@@ -25,25 +33,32 @@ gql`
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
standard
|
||||
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 +68,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 +83,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 +113,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 +140,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 +149,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 +165,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]
|
||||
)
|
||||
}
|
||||
|
||||
51
src/graphql/data/TrendingTokens.ts
Normal file
51
src/graphql/data/TrendingTokens.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import gql from 'graphql-tag'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useTrendingTokensQuery } from './__generated__/types-and-hooks'
|
||||
import { chainIdToBackendName, unwrapToken } from './util'
|
||||
|
||||
gql`
|
||||
query TrendingTokens($chain: Chain!) {
|
||||
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function useTrendingTokens(chainId?: number) {
|
||||
const chain = chainIdToBackendName(chainId)
|
||||
const { data, loading } = useTrendingTokensQuery({ variables: { chain } })
|
||||
|
||||
return useMemo(
|
||||
() => ({ data: data?.topTokens?.map((token) => unwrapToken(chainId ?? 1, token)), loading }),
|
||||
[chainId, data?.topTokens, loading]
|
||||
)
|
||||
}
|
||||
504
src/graphql/data/__generated__/types-and-hooks.ts
generated
504
src/graphql/data/__generated__/types-and-hooks.ts
generated
@@ -95,6 +95,11 @@ export enum Currency {
|
||||
Usd = 'USD'
|
||||
}
|
||||
|
||||
export enum DatasourceProvider {
|
||||
Alternate = 'ALTERNATE',
|
||||
Legacy = 'LEGACY'
|
||||
}
|
||||
|
||||
export type Dimensions = {
|
||||
__typename?: 'Dimensions';
|
||||
height?: Maybe<Scalars['Float']>;
|
||||
@@ -386,6 +391,7 @@ export type NftCollectionTraitStats = {
|
||||
|
||||
export type NftCollectionsFilterInput = {
|
||||
addresses?: InputMaybe<Array<Scalars['String']>>;
|
||||
nameQuery?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type NftContract = IContract & {
|
||||
@@ -468,12 +474,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 +543,36 @@ 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;
|
||||
};
|
||||
|
||||
export type PermitDetailsInput = {
|
||||
amount: Scalars['String'];
|
||||
expiration: Scalars['String'];
|
||||
nonce: Scalars['String'];
|
||||
token: Scalars['String'];
|
||||
};
|
||||
|
||||
export type PermitInput = {
|
||||
details: PermitDetailsInput;
|
||||
sigDeadline: Scalars['String'];
|
||||
signature: Scalars['String'];
|
||||
spender: Scalars['String'];
|
||||
};
|
||||
|
||||
/** 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 +598,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 +613,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 +651,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 +731,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 +765,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 +877,31 @@ export enum TokenStandard {
|
||||
Native = 'NATIVE'
|
||||
}
|
||||
|
||||
export type TokenTradeInput = {
|
||||
permit?: InputMaybe<PermitInput>;
|
||||
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 +914,11 @@ export type TokenTransfer = {
|
||||
transactedValue?: Maybe<Amount>;
|
||||
};
|
||||
|
||||
export type TradePoolInput = {
|
||||
pair?: InputMaybe<PairInput>;
|
||||
pool?: InputMaybe<PoolInput>;
|
||||
};
|
||||
|
||||
export type Transaction = {
|
||||
__typename?: 'Transaction';
|
||||
blockNumber: Scalars['Int'];
|
||||
@@ -825,20 +944,37 @@ export enum TransactionStatus {
|
||||
Pending = 'PENDING'
|
||||
}
|
||||
|
||||
export type TokenQueryVariables = Exact<{
|
||||
contract: ContractInput;
|
||||
export type RecentlySearchedAssetsQueryVariables = Exact<{
|
||||
collectionAddresses: Array<Scalars['String']> | Scalars['String'];
|
||||
contracts: Array<ContractInput> | ContractInput;
|
||||
}>;
|
||||
|
||||
|
||||
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 RecentlySearchedAssetsQuery = { __typename?: 'Query', nftCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', node: { __typename?: 'NftCollection', collectionId: string, isVerified?: boolean, name?: string, numAssets?: number, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, value: number } }> } }> }, tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
|
||||
|
||||
export type SearchTokensQueryVariables = Exact<{
|
||||
searchQuery: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SearchTokensQuery = { __typename?: 'Query', searchTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
|
||||
|
||||
export type TokenQueryVariables = Exact<{
|
||||
chain: Chain;
|
||||
address?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, 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 +982,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, standard?: TokenStandard, 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 +990,14 @@ 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 TrendingTokensQueryVariables = Exact<{
|
||||
chain: Chain;
|
||||
}>;
|
||||
|
||||
|
||||
export type TrendingTokensQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
|
||||
|
||||
export type AssetQueryVariables = Exact<{
|
||||
address: Scalars['String'];
|
||||
@@ -895,44 +1038,212 @@ 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 RecentlySearchedAssetsDocument = gql`
|
||||
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
|
||||
nftCollections(filter: {addresses: $collectionAddresses}) {
|
||||
edges {
|
||||
node {
|
||||
collectionId
|
||||
image {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
name
|
||||
numAssets
|
||||
nftContracts {
|
||||
address
|
||||
}
|
||||
markets(currencies: ETH) {
|
||||
floorPrice {
|
||||
currency
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens(contracts: $contracts) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRecentlySearchedAssetsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRecentlySearchedAssetsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRecentlySearchedAssetsQuery` 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 } = useRecentlySearchedAssetsQuery({
|
||||
* variables: {
|
||||
* collectionAddresses: // value for 'collectionAddresses'
|
||||
* contracts: // value for 'contracts'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRecentlySearchedAssetsQuery(baseOptions: Apollo.QueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
|
||||
}
|
||||
export function useRecentlySearchedAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
|
||||
}
|
||||
export type RecentlySearchedAssetsQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsQuery>;
|
||||
export type RecentlySearchedAssetsLazyQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsLazyQuery>;
|
||||
export type RecentlySearchedAssetsQueryResult = Apollo.QueryResult<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>;
|
||||
export const SearchTokensDocument = gql`
|
||||
query SearchTokens($searchQuery: String!) {
|
||||
searchTokens(searchQuery: $searchQuery) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useSearchTokensQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useSearchTokensQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useSearchTokensQuery` 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 } = useSearchTokensQuery({
|
||||
* variables: {
|
||||
* searchQuery: // value for 'searchQuery'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSearchTokensQuery(baseOptions: Apollo.QueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
|
||||
}
|
||||
export function useSearchTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
|
||||
}
|
||||
export type SearchTokensQueryHookResult = ReturnType<typeof useSearchTokensQuery>;
|
||||
export type SearchTokensLazyQueryHookResult = ReturnType<typeof useSearchTokensLazyQuery>;
|
||||
export type SearchTokensQueryResult = Apollo.QueryResult<SearchTokensQuery, SearchTokensQueryVariables>;
|
||||
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
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
standard
|
||||
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 +1264,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 +1281,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 +1314,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'
|
||||
* },
|
||||
* });
|
||||
@@ -1020,25 +1339,32 @@ export const TopTokens100Document = gql`
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
standard
|
||||
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 +1402,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
|
||||
}
|
||||
@@ -1115,6 +1445,69 @@ export function useTopTokensSparklineLazyQuery(baseOptions?: Apollo.LazyQueryHoo
|
||||
export type TopTokensSparklineQueryHookResult = ReturnType<typeof useTopTokensSparklineQuery>;
|
||||
export type TopTokensSparklineLazyQueryHookResult = ReturnType<typeof useTopTokensSparklineLazyQuery>;
|
||||
export type TopTokensSparklineQueryResult = Apollo.QueryResult<TopTokensSparklineQuery, TopTokensSparklineQueryVariables>;
|
||||
export const TrendingTokensDocument = gql`
|
||||
query TrendingTokens($chain: Chain!) {
|
||||
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTrendingTokensQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTrendingTokensQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTrendingTokensQuery` 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 } = useTrendingTokensQuery({
|
||||
* variables: {
|
||||
* chain: // value for 'chain'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTrendingTokensQuery(baseOptions: Apollo.QueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
|
||||
}
|
||||
export function useTrendingTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
|
||||
}
|
||||
export type TrendingTokensQueryHookResult = ReturnType<typeof useTrendingTokensQuery>;
|
||||
export type TrendingTokensLazyQueryHookResult = ReturnType<typeof useTrendingTokensLazyQuery>;
|
||||
export type TrendingTokensQueryResult = Apollo.QueryResult<TrendingTokensQuery, TrendingTokensQueryVariables>;
|
||||
export const AssetDocument = gql`
|
||||
query Asset($address: String!, $orderBy: NftAssetSortableField, $asc: Boolean, $filter: NftAssetsFilterInput, $first: Int, $after: String, $last: Int, $before: String) {
|
||||
nftAssets(
|
||||
@@ -1487,6 +1880,7 @@ export const NftBalanceDocument = gql`
|
||||
url
|
||||
}
|
||||
name
|
||||
twitterName
|
||||
nftContracts {
|
||||
address
|
||||
chain
|
||||
@@ -1586,4 +1980,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,30 @@
|
||||
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,
|
||||
@@ -74,17 +95,8 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
|
||||
|
||||
export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo]
|
||||
|
||||
export function getTokenDetailsURL(address: string, chainName?: Chain, chainId?: number) {
|
||||
if (address === ZERO_ADDRESS && chainId && chainId === SupportedChainId.MAINNET) {
|
||||
return `/tokens/${CHAIN_ID_TO_BACKEND_NAME[chainId].toLowerCase()}/${NATIVE_CHAIN_ID}`
|
||||
} else if (chainName) {
|
||||
return `/tokens/${chainName.toLowerCase()}/${address}`
|
||||
} else if (chainId) {
|
||||
const chainName = CHAIN_ID_TO_BACKEND_NAME[chainId]
|
||||
return chainName ? `/tokens/${chainName.toLowerCase()}/${address}` : ''
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
export function getTokenDetailsURL({ address, chain }: { address?: string | null; chain: Chain }) {
|
||||
return `/tokens/${chain.toLowerCase()}/${address ?? NATIVE_CHAIN_ID}`
|
||||
}
|
||||
|
||||
export function unwrapToken<
|
||||
|
||||
@@ -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']>;
|
||||
|
||||
@@ -9,30 +9,25 @@ import { useAppDispatch } from 'state/hooks'
|
||||
|
||||
import { fetchTokenList } from '../state/lists/actions'
|
||||
|
||||
export function useFetchListCallback(): (
|
||||
listUrl: string,
|
||||
sendDispatch?: boolean,
|
||||
skipValidation?: boolean
|
||||
) => Promise<TokenList> {
|
||||
export function useFetchListCallback(): (listUrl: string, skipValidation?: boolean) => Promise<TokenList> {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// note: prevent dispatch if using for list search or unsupported list
|
||||
return useCallback(
|
||||
async (listUrl: string, sendDispatch = true, skipValidation?: boolean) => {
|
||||
async (listUrl: string, skipValidation?: boolean) => {
|
||||
const requestId = nanoid()
|
||||
sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
return getTokenList(
|
||||
listUrl,
|
||||
(ensName: string) => resolveENSContentHash(ensName, RPC_PROVIDERS[SupportedChainId.MAINNET]),
|
||||
skipValidation
|
||||
)
|
||||
.then((tokenList) => {
|
||||
sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
return tokenList
|
||||
})
|
||||
.catch((error) => {
|
||||
console.debug(`Failed to get list at url ${listUrl}`, error)
|
||||
sendDispatch && dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
throw error
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -57,7 +57,7 @@ export function useUniversalRouterSwapCallback(
|
||||
gasEstimate = await provider.estimateGas(tx)
|
||||
} catch (gasError) {
|
||||
console.warn(gasError)
|
||||
throw new InvalidSwapError('Gas estimation failed. Wait a few minutes and try again.')
|
||||
throw new Error('Your swap is expected to fail')
|
||||
}
|
||||
const gasLimit = calculateGasMargin(gasEstimate)
|
||||
const response = await provider
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FungibleToken, GenieCollection } from 'nft/types'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import { GenieCollection } from 'nft/types'
|
||||
|
||||
/**
|
||||
* Organizes the number of Token and NFT results to be shown to a user depending on if they're in the NFT or Token experience
|
||||
@@ -10,9 +11,9 @@ import { FungibleToken, GenieCollection } from 'nft/types'
|
||||
*/
|
||||
export function organizeSearchResults(
|
||||
isNFTPage: boolean,
|
||||
tokenResults: FungibleToken[],
|
||||
tokenResults: SearchToken[],
|
||||
collectionResults: GenieCollection[]
|
||||
): [FungibleToken[], GenieCollection[]] {
|
||||
): [SearchToken[], GenieCollection[]] {
|
||||
const reducedTokens =
|
||||
tokenResults?.slice(0, isNFTPage ? 3 : collectionResults.length < 3 ? 8 - collectionResults.length : 5) ?? []
|
||||
const reducedCollections = collectionResults.slice(0, 8 - reducedTokens.length)
|
||||
|
||||
@@ -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,28 +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 { 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;
|
||||
@@ -39,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;
|
||||
`
|
||||
@@ -65,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;
|
||||
@@ -85,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>
|
||||
)
|
||||
@@ -114,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>
|
||||
@@ -129,22 +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 inputCurrency = useCurrency('ETH')
|
||||
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(() => {
|
||||
@@ -154,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)
|
||||
@@ -178,88 +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 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={inputCurrency} size="24px" />
|
||||
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
|
||||
{inputCurrency?.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())} 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 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,100 +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;
|
||||
}
|
||||
`
|
||||
|
||||
export const BagTokenSelectorModal = ({ overlayClick }: { overlayClick: () => void }) => {
|
||||
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} />
|
||||
})}
|
||||
</TokenSelectorContainer>
|
||||
</ModalWrapper>
|
||||
<Overlay onClick={overlayClick} />
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -1,55 +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 styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const TokenRow = styled(Row)`
|
||||
padding: 8px 0px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const TokenInfoRow = styled(Row)`
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const StyledBalanceText = styled(ThemedText.SubHeader)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
export const CurrencyRow = ({ currency }: { currency: Currency }) => {
|
||||
const { account } = useWeb3React()
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
|
||||
return (
|
||||
<TokenRow>
|
||||
<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} />}
|
||||
</TokenRow>
|
||||
)
|
||||
}
|
||||
|
||||
const Balance = ({ balance }: { balance: CurrencyAmount<Currency> }) => {
|
||||
return (
|
||||
<StyledBalanceText fontWeight={500} lineHeight="24px">
|
||||
{balance.toSignificant(4)}
|
||||
</StyledBalanceText>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ export const modalContainer = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
position: 'fixed',
|
||||
flexWrap: 'wrap',
|
||||
height: 'full',
|
||||
width: { sm: 'full', md: 'min' },
|
||||
left: { sm: '0', md: '1/2' },
|
||||
|
||||
@@ -201,7 +201,6 @@ const TxCompleteModal = () => {
|
||||
>
|
||||
<Box className={styles.mixedRefundModal} onClick={stopPropagation}>
|
||||
<Box
|
||||
height="full"
|
||||
display="inline-flex"
|
||||
flexWrap="wrap"
|
||||
width={{ sm: 'full', md: 'half' }}
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
@@ -8,7 +8,6 @@ export * from './useMarketplaceSelect'
|
||||
export * from './useNFTList'
|
||||
export * from './useNFTSelect'
|
||||
export * from './useProfilePageState'
|
||||
export * from './useSearchHistory'
|
||||
export * from './useSelectAsset'
|
||||
export * from './useSellAsset'
|
||||
export * from './useSendTransaction'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user