Compare commits

...

27 Commits

Author SHA1 Message Date
Jack Short
2b5769ac86 chore: removing VE from cards (#6038)
* moving cards to styled components

* converting rows to styled components

* image

* finished images

* handling all media

* removed VE from cards

* no content colors

* removing aspect ratio calc

* responding to all comments
2023-02-28 16:49:51 -05:00
Charles Bachmeier
b811afd134 feat: add new statsig backed feature flag for nft graphql migration (#6044)
* NFT-1113 add new statsig backed feature flag for nft graphql migration

* remove comment

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-28 13:38:10 -08:00
Charles Bachmeier
6131e6bfab chore: Cleanup and refactor significant portion of listing logic (#6042)
* NFT-91 move listing mode to shared

* move expiration setting out of useEffect

* simplify price input color logic

* unused boolean

* simplify removal of local listing market

* handle global same pricing

* undo local market change

* added comment

* undo expiration changes

* remove old listing state logic

* formatting

* small cleanup

* cleanup

* deprecate global listing state

* use stablecoin values

* remove unused pausing functionality

* remove unused pausing functionality

* remove unused warning logic

* remove unused royalty field

* use royalty helper fn

* simplify global vs normal price input

* simplified price setting logic

* price inputs need to respond to global price method changes

* slight simplifcations

* move dynamic price logic to hook

* move utils file

* move enum to shared

* only usdc check

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-28 11:47:22 -08:00
Vignesh Mohankumar
5979635939 chore: update coinbase-wallet sdk (#6037) 2023-02-27 17:18:21 -05:00
dependabot[bot]
5399bdb550 chore(deps): bump @uniswap/widgets from 2.39.0 to 2.40.0 (#6032)
Bumps [@uniswap/widgets](https://github.com/Uniswap/widgets) from 2.39.0 to 2.40.0.
- [Release notes](https://github.com/Uniswap/widgets/releases)
- [Changelog](https://github.com/Uniswap/widgets/blob/main/.releaserc.json)
- [Commits](https://github.com/Uniswap/widgets/compare/v2.39.0...v2.40.0)

---
updated-dependencies:
- dependency-name: "@uniswap/widgets"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-27 15:01:55 -05:00
Charles Bachmeier
1df9da9eff chore: Refactor ListingButton with deprecation of Listing V1 (#6023)
* remove unused wrapper

* remove old listing modal

* simplify button styling

* deprecate outdated listing logic and move button to styled components

* remove unused linting protection

* remove unused functions from sellAssets hook

* undo and save this refactor for different PR

* remove more unused items

* hook dependencies

* more trash cleanup

* styled continue button

* slight continue button tweaks

* cleanup

* add new standard hover state

* no mixed conditionals

* lint

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-27 10:42:30 -08:00
Jordan Frankfurt
b1e6d0ab7a chore: schema changed gen types (#6033)
schema changed gen types
2023-02-27 13:03:20 -05:00
eddie
a7fcbb4cfc revert: dont hideConnectionUI for widget (#6030)
Revert "fix: dont hideConnectionUI for widget (#6010)"

This reverts commit c18522159b.
2023-02-27 09:18:47 -08:00
Moody Salem
a5a6a037e5 ci: references to old id upload (#6034)
fix: references to old id `upload`

`upload` seems to have been changed to `pinata`
2023-02-27 09:09:50 -08:00
Andrew MacPherson
8bfebd37a2 Updating .env.production file to also include the same comment as .env about reporting these public API keys 2023-02-27 10:00:22 -05:00
Vignesh Mohankumar
4029819090 chore: update widget (#6029) 2023-02-24 17:27:08 -05:00
Vignesh Mohankumar
021ae5e74e fix: reset tokens, amounts when switching chains (#6026)
* fix: reset tokens when switching chains on widget

* clear amounts
2023-02-24 16:35:53 -05:00
eddie
2b9720705f feat: log errors on swap page (#6008)
* feat: log error from widget

* fix: upgrade analytics

* feat: add swap error to main Error Boundary too
2023-02-24 12:09:41 -08:00
blairmason
8f1ea32e5e feat: config statsig init to use reverse proxy (#6018)
* config statsig init to use reverse proxy

* formatting fixes

* update statsig proxy endpoints to prod url
2023-02-24 09:28:01 -08:00
eddie
23acb3b395 feat: add statsig feature gate for widget (#6011)
* feat: add statsig feature gate for widget

* feat: move statsig check into featureFlag infra

* fix: move statsig check deeper into feature flag code

* Update src/featureFlags/flags/featureFlags.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/featureFlags/flags/dummyFeatureGate.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-02-23 12:47:26 -08:00
Charles Bachmeier
772416cc7a feat: add collection blocklist for ip infringement (#6022)
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-23 10:44:27 -08:00
Jordan Frankfurt
f15e5725f1 fix: token list load issue WEB-2083 (#6019) 2023-02-22 22:24:30 -06:00
Jordan Frankfurt
83c8393f19 fix: back arrow from migration should take you to v3 pool page (#6020) 2023-02-22 22:24:22 -06:00
Jack Short
1348eb3322 fix: token selector correct zIndex on mobile for pwat (#6017) 2023-02-22 20:43:35 -05:00
eddie
a2271ba428 fix: widget defaultChainId (#6012) 2023-02-22 14:14:12 -08:00
Charles Bachmeier
1845cb3b7b chore: remove listv2 feature flag (#6009)
* remove listv2 feature flag

* no longer used styles and icons

* missed one

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-22 09:43:39 -08:00
Jordan Frankfurt
6a02bde8e0 fix: darkmode FoR iframe wrapper background color (#5993)
* fix: darkmode FoR iframe wrapper background color

* pr feedback
2023-02-22 10:38:52 -06:00
eddie
c18522159b fix: dont hideConnectionUI for widget (#6010) 2023-02-22 09:23:56 -06:00
Jack Short
5ea7b1de3f feat: enable pay with any token (#6002) 2023-02-22 08:25:09 -05:00
Charles Bachmeier
6efe8f3260 feat: [ListV2] Several Small Polish Changes (#5998)
* NFT-1075 update floor and last price to use body primary

* NFT-1080 update image thumbnail size and adjusted row accordingly

* NFT-1081 force page refresh on success screen

* NFT-1089 update same price icons

* NFT-1082 show warning colors over focus

* remove unused state var

* replace margin with padding for sticky header

* NFT-1109 properly calc if listing date is over 6mo

* NFT-1076 change listing button text to light and mobile profile bar to backgroundSurface

* NFT-1079 persist row data when listing to multiple markets

* perf improvement

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-21 13:56:26 -08:00
lynn
9ac28a4571 fix: token selector scroll margin (#6005)
* fix

* update tests

* remove gql file

* fix breakng snapshot
2023-02-21 16:40:36 -05:00
eddie
bde1421ffb fix: use ipfs token list as fallback for unsupported list (#6007) 2023-02-21 12:55:40 -08:00
61 changed files with 1431 additions and 2298 deletions

2
.env
View File

@@ -1,5 +1,6 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern.
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
@@ -10,4 +11,3 @@ REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
REACT_APP_STATSIG_API_KEY="client-1rY92WZGidd2hgW4x1lsZ7afqm1Qfr3sJfH3A5b8eJa"

View File

@@ -1,4 +1,6 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern.
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
@@ -9,4 +11,3 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
REACT_APP_SENTRY_ENABLED=false
REACT_APP_STATSIG_API_KEY="client-1rY92WZGidd2hgW4x1lsZ7afqm1Qfr3sJfH3A5b8eJa"

View File

@@ -62,7 +62,7 @@ jobs:
continue-on-error: true
timeout-minutes: 2
with:
cid: ${{ steps.upload.outputs.hash }}
cid: ${{ steps.pinata.outputs.hash }}
seeds: ${{ secrets.CRUST_SEEDS }}
- name: Convert CIDv0 to CIDv1
@@ -93,7 +93,7 @@ jobs:
IPFS gateways:
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.dweb.link/
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.pinata.outputs.hash }}/)
- [ipfs://${{ steps.pinata.outputs.hash }}/](ipfs://${{ steps.pinata.outputs.hash }}/)
${{ needs.tag.outputs.changelog }}

View File

@@ -111,7 +111,7 @@
},
"dependencies": {
"@apollo/client": "^3.7.2",
"@coinbase/wallet-sdk": "^3.3.0",
"@coinbase/wallet-sdk": "^3.6.4",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@graphql-codegen/cli": "^2.15.0",
@@ -133,8 +133,8 @@
"@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "^7.29.0",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "^1.3.0",
"@uniswap/analytics-events": "^2.3.0",
"@uniswap/analytics": "^1.3.1",
"@uniswap/analytics-events": "^2.4.0",
"@uniswap/conedison": "^1.3.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
@@ -152,7 +152,7 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "^2.29.3",
"@uniswap/widgets": "^2.40.0",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",

View File

@@ -1,10 +1,13 @@
import { Trans } from '@lingui/macro'
import * as Sentry from '@sentry/react'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { SwapEventName } from '@uniswap/analytics-events'
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
import { ChevronUpIcon } from 'nft/components/icons'
import { useIsMobile } from 'nft/hooks'
import React, { PropsWithChildren, useState } from 'react'
import { Copy } from 'react-feather'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import { isSentryEnabled } from 'utils/env'
@@ -217,13 +220,19 @@ const updateServiceWorkerInBackground = async () => {
}
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
const { pathname } = useLocation()
return (
<Sentry.ErrorBoundary
fallback={({ error, eventId }) => <Fallback error={error} eventId={eventId} />}
beforeCapture={(scope) => {
scope.setLevel('fatal')
}}
onError={updateServiceWorkerInBackground}
onError={(error) => {
updateServiceWorkerInBackground()
if (pathname === '/swap') {
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, { error })
}
}}
>
{children}
</Sentry.ErrorBoundary>

View File

@@ -1,6 +1,6 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql'
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
@@ -212,12 +212,6 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.permit2}
label="Permit 2 / Universal Router"
/>
<FeatureFlagOption
variant={NftListV2Variant}
value={useNftListV2Flag()}
featureFlag={FeatureFlag.nftListV2}
label="NFT Listing Page v2"
/>
<FeatureFlagOption
variant={PayWithAnyTokenVariant}
value={usePayWithAnyTokenFlag()}
@@ -236,6 +230,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.gqlRouting}
label="GraphQL NFT Routing"
/>
<FeatureFlagOption
variant={NftGraphqlVariant}
value={useNftGraphqlFlag()}
featureFlag={FeatureFlag.nftGraphql}
label="Migrate NFT read endpoints to GQL"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
variant={TraceJsonRpcVariant}

View File

@@ -10,8 +10,10 @@ import { CustomLightSpinner, ThemedText } from 'theme'
import Circle from '../../assets/images/blue-loader.svg'
import Modal from '../Modal'
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.white};
const MOONPAY_DARK_BACKGROUND = '#1c1c1e'
const Wrapper = styled.div<{ isDarkMode: boolean }>`
// #1c1c1e is the background color for the darkmode moonpay iframe as of 2/16/2023
background-color: ${({ isDarkMode, theme }) => (isDarkMode ? MOONPAY_DARK_BACKGROUND : theme.white)};
border-radius: 20px;
box-shadow: ${({ theme }) => theme.deepShadow};
display: flex;
@@ -29,8 +31,9 @@ const ErrorText = styled(ThemedText.BodyPrimary)`
text-align: center;
width: 90%;
`
const StyledIframe = styled.iframe`
background-color: ${({ theme }) => theme.white};
const StyledIframe = styled.iframe<{ isDarkMode: boolean }>`
// #1c1c1e is the background color for the darkmode moonpay iframe as of 2/16/2023
background-color: ${({ isDarkMode, theme }) => (isDarkMode ? MOONPAY_DARK_BACKGROUND : theme.white)};
border-radius: 12px;
bottom: 0;
left: 0;
@@ -123,7 +126,7 @@ export default function FiatOnrampModal() {
return (
<Modal isOpen={fiatOnrampModalOpen} onDismiss={closeModal} maxHeight={720}>
<Wrapper data-testid="fiat-onramp-modal">
<Wrapper data-testid="fiat-onramp-modal" isDarkMode={isDarkMode}>
{error ? (
<>
<ThemedText.MediumHeader>
@@ -138,7 +141,12 @@ export default function FiatOnrampModal() {
) : loading ? (
<StyledSpinner src={Circle} alt="loading spinner" size="90px" />
) : (
<StyledIframe src={signedIframeUrl ?? ''} frameBorder="0" title="fiat-onramp-iframe" />
<StyledIframe
src={signedIframeUrl ?? ''}
frameBorder="0"
title="fiat-onramp-iframe"
isDarkMode={isDarkMode}
/>
)}
</Wrapper>
</Modal>

View File

@@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import Web3Status from 'components/Web3Status'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { chainIdToBackendName } from 'graphql/data/util'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { Box } from 'nft/components/Box'
@@ -82,7 +81,6 @@ export const PageTabs = () => {
const Navbar = () => {
const isNftPage = useIsNftPage()
const sellPageState = useProfilePageState((state) => state.state)
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const navigate = useNavigate()
return (
@@ -124,7 +122,7 @@ const Navbar = () => {
<Box display={{ sm: 'none', lg: 'flex' }}>
<MenuDropdown />
</Box>
{isNftPage && (!isNftListV2 || sellPageState !== ProfilePageStateType.LISTING) && <Bag />}
{isNftPage && sellPageState !== ProfilePageStateType.LISTING && <Bag />}
{!isNftPage && (
<Box display={{ sm: 'none', lg: 'flex' }}>
<ChainSelector />

View File

@@ -105,7 +105,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
}
<div
style="padding-right: 8px; padding-top: 8px;"
style="padding-right: 4px;"
>
<div
class="CurrencyList_scrollbarStyle__1pi21y70"
@@ -388,7 +388,7 @@ exports[`renders loading rows when isLoading is true 1`] = `
}
<div
style="padding-right: 8px; padding-top: 8px;"
style="padding-right: 4px;"
>
<div
class="CurrencyList_scrollbarStyle__1pi21y70"

View File

@@ -290,7 +290,7 @@ export default function CurrencyList({
}, [])
return (
<div style={{ paddingRight: '8px', paddingTop: '8px' }}>
<div style={{ paddingRight: '4px' }}>
{isLoading ? (
<FixedSizeList
className={styles.scrollbarStyle}

View File

@@ -3,11 +3,10 @@ import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { FeatureGate } from 'featureFlags/flags/featureFlags'
import { useDummyGateEnabled } from 'featureFlags/flags/dummyFeatureGate'
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { useGate } from 'statsig-react'
import styled from 'styled-components/macro'
import { StyledInternalLink } from 'theme'
@@ -89,7 +88,7 @@ export default function MobileBalanceSummaryFooter({ token }: { token: Currency
const formattedBalance = formatCurrencyAmount(balance, NumberType.TokenNonTx)
const formattedUsdValue = formatCurrencyAmount(useStablecoinValue(balance), NumberType.FiatTokenStats)
const chain = CHAIN_ID_TO_BACKEND_NAME[token.chainId].toLowerCase()
const { value: isDummyGateFlagEnabled } = useGate(FeatureGate.DUMMY)
const isDummyGateFlagEnabled = useDummyGateEnabled()
return (
<Wrapper>

View File

@@ -57,7 +57,7 @@ export default function Widget({
onDefaultTokenChange,
onReviewSwapClick,
}: WidgetProps) {
const { connector, provider } = useWeb3React()
const { connector, provider, chainId } = useWeb3React()
const locale = useActiveLocale()
const theme = useWidgetTheme()
const { inputs, tokenSelector } = useSyncWidgetInputs({
@@ -169,7 +169,7 @@ export default function Widget({
locale={locale}
theme={theme}
width={width}
// defaultChainId is excluded - it is always inferred from the passed provider
defaultChainId={chainId}
onConnectWalletClick={onConnectWalletClick}
provider={provider}
onSwitchChain={onSwitchChain}
@@ -183,6 +183,9 @@ export default function Widget({
onSwapApprove={onApproveToken}
onInitialSwapQuote={onInitialSwapQuote}
onSwapPriceUpdateAck={onSwapPriceUpdateAck}
onError={(error, errorInfo) => {
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, { error, errorInfo, ...trace })
}}
/>
{tokenSelector}
</>

View File

@@ -1,7 +1,9 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfaceSectionName, SwapEventName } from '@uniswap/analytics-events'
import { Currency, Field, SwapController, SwapEventHandlers, TradeType } from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
import usePrevious from 'hooks/usePrevious'
import { useCallback, useEffect, useMemo, useState } from 'react'
const EMPTY_AMOUNT = ''
@@ -29,24 +31,35 @@ export function useSyncWidgetInputs({
}) {
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
const { chainId } = useWeb3React()
const previousChainId = usePrevious(chainId)
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
const [tokens, setTokens] = useState<SwapTokens>(defaultTokens)
useEffect(() => {
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
setTokens({
...tokens,
[Field.INPUT]: defaultTokens[Field.INPUT] ?? tokens[Field.INPUT],
[Field.OUTPUT]: defaultTokens[Field.OUTPUT] ?? tokens[Field.OUTPUT] ?? defaultTokens.default,
default: defaultTokens.default,
})
}
}, [defaultTokens, tokens])
useEffect(() => {
if (chainId !== previousChainId && !!previousChainId) {
setTokens({
...tokens,
[Field.INPUT]: undefined,
[Field.OUTPUT]: undefined,
})
setAmount(EMPTY_AMOUNT)
}
}, [chainId, previousChainId, tokens])
const onAmountChange = useCallback(
(field: Field, amount: string, origin?: 'max') => {
if (origin === 'max') {

View File

@@ -9,8 +9,12 @@ class TokenLogoLookupTable {
initialize() {
const dict: { [key: string]: string[] | undefined } = {}
DEFAULT_LIST_OF_LISTS.forEach((list) =>
store.getState().lists.byUrl[list].current?.tokens.forEach((token) => {
DEFAULT_LIST_OF_LISTS.forEach((list) => {
const listData = store.getState().lists.byUrl[list]
if (!listData) {
return
}
listData.current?.tokens.forEach((token) => {
if (token.logoURI) {
const lowercaseAddress = token.address.toLowerCase()
const currentEntry = dict[lowercaseAddress + ':' + token.chainId]
@@ -21,7 +25,7 @@ class TokenLogoLookupTable {
}
}
})
)
})
this.dict = dict
this.initialized = true
}

View File

@@ -1,6 +1,6 @@
export const UNI_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
export const UNI_EXTENDED_LIST = 'https://extendedtokens.uniswap.org/'
const UNI_UNSUPPORTED_LIST = 'https://unsupportedtokens.uniswap.org/'
export const UNI_EXTENDED_LIST = 'https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org'
const UNI_UNSUPPORTED_LIST = 'https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org'
const AAVE_LIST = 'tokenlist.aave.eth'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
const CMC_ALL_LIST = 'https://api.coinmarketcap.com/data-api/v3/uniswap/all.json'

View File

@@ -0,0 +1,9 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
function useDummyGateFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.statsigDummy)
}
export function useDummyGateEnabled(): boolean {
return useDummyGateFlag() === BaseVariant.Enabled
}

View File

@@ -1,12 +1,12 @@
/**
* The value here must match the value in the statsig dashboard, if you plan to use statsig.
*/
export enum FeatureFlag {
traceJsonRpc = 'traceJsonRpc',
permit2 = 'permit2',
nftListV2 = 'nftListV2',
payWithAnyToken = 'payWithAnyToken',
swapWidget = 'swapWidget',
swapWidget = 'swap_widget_replacement_enabled',
gqlRouting = 'gqlRouting',
}
export enum FeatureGate {
DUMMY = 'web_dummy_gate_amplitude_id',
statsigDummy = 'web_dummy_gate_amplitude_id',
nftGraphql = 'nft_graphql_migration',
}

View File

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

View File

@@ -1,7 +0,0 @@
import { BaseVariant } from '../index'
export function useNftListV2Flag(): BaseVariant {
return BaseVariant.Enabled
}
export { BaseVariant as NftListV2Variant }

View File

@@ -0,0 +1,7 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useNftGraphqlFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.nftGraphql)
}
export { BaseVariant as NftGraphqlVariant }

View File

@@ -4,7 +4,7 @@ import { useWeb3React } from '@web3-react/core'
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function usePayWithAnyTokenFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.payWithAnyToken)
return useBaseFlag(FeatureFlag.payWithAnyToken, BaseVariant.Enabled)
}
export function usePayWithAnyTokenEnabled(): boolean {

View File

@@ -1,5 +1,6 @@
import { atomWithStorage, useAtomValue, useUpdateAtom } from 'jotai/utils'
import { createContext, ReactNode, useCallback, useContext } from 'react'
import { useGate } from 'statsig-react'
export { FeatureFlag } from './flags/featureFlags'
interface FeatureFlagsContextType {
@@ -56,7 +57,12 @@ export enum BaseVariant {
}
export function useBaseFlag(flag: string, defaultValue = BaseVariant.Control): BaseVariant {
switch (useFeatureFlagsContext().flags[flag]) {
const { value: statsigValue } = useGate(flag) // non-existent gates return false
const featureFlagsContext = useFeatureFlagsContext()
if (statsigValue) {
return BaseVariant.Enabled
}
switch (featureFlagsContext.flags[flag]) {
case 'enabled':
return BaseVariant.Enabled
case 'control':

View File

@@ -86,6 +86,10 @@ export enum Chain {
UnknownChain = 'UNKNOWN_CHAIN'
}
export enum CollectionSortableField {
Volume = 'VOLUME'
}
export type ContractInput = {
address?: InputMaybe<Scalars['String']>;
chain: Chain;
@@ -96,11 +100,6 @@ export enum Currency {
Usd = 'USD'
}
export enum DatasourceProvider {
Alternate = 'ALTERNATE',
Legacy = 'LEGACY'
}
export type Dimensions = {
__typename?: 'Dimensions';
height?: Maybe<Scalars['Float']>;
@@ -145,6 +144,49 @@ export enum MarketSortableField {
Volume = 'VOLUME'
}
export type NftActivity = {
__typename?: 'NftActivity';
address: Scalars['String'];
asset?: Maybe<NftAsset>;
fromAddress: Scalars['String'];
id: Scalars['ID'];
marketplace?: Maybe<NftMarketplace>;
orderStatus?: Maybe<OrderStatus>;
price?: Maybe<Amount>;
quantity?: Maybe<Scalars['Int']>;
timestamp: Scalars['Int'];
toAddress?: Maybe<Scalars['String']>;
tokenId?: Maybe<Scalars['String']>;
transactionHash?: Maybe<Scalars['String']>;
type: NftActivityType;
url?: Maybe<Scalars['String']>;
};
export type NftActivityConnection = {
__typename?: 'NftActivityConnection';
edges: Array<NftActivityEdge>;
pageInfo: PageInfo;
};
export type NftActivityEdge = {
__typename?: 'NftActivityEdge';
cursor: Scalars['String'];
node: NftActivity;
};
export type NftActivityFilterInput = {
activityTypes?: InputMaybe<Array<NftActivityType>>;
address?: InputMaybe<Scalars['String']>;
tokenId?: InputMaybe<Scalars['String']>;
};
export enum NftActivityType {
CancelListing = 'CANCEL_LISTING',
Listing = 'LISTING',
Sale = 'SALE',
Transfer = 'TRANSFER'
}
export type NftApproval = {
__typename?: 'NftApproval';
approvedAddress: Scalars['String'];
@@ -196,7 +238,6 @@ export type NftAssetListingsArgs = {
after?: InputMaybe<Scalars['String']>;
asc?: InputMaybe<Scalars['Boolean']>;
before?: InputMaybe<Scalars['String']>;
datasource?: InputMaybe<DatasourceProvider>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
@@ -311,7 +352,6 @@ export type NftCollection = {
export type NftCollectionMarketsArgs = {
currencies: Array<Currency>;
datasource?: InputMaybe<DatasourceProvider>;
};
export type NftCollectionConnection = {
@@ -335,6 +375,8 @@ export type NftCollectionMarket = {
marketplaces?: Maybe<Array<NftCollectionMarketplace>>;
nftContracts?: Maybe<Array<NftContract>>;
owners?: Maybe<Scalars['Int']>;
percentListed?: Maybe<TimestampedAmount>;
percentUniqueOwners?: Maybe<TimestampedAmount>;
sales?: Maybe<TimestampedAmount>;
totalVolume?: Maybe<TimestampedAmount>;
volume?: Maybe<TimestampedAmount>;
@@ -602,6 +644,7 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
export type Query = {
__typename?: 'Query';
nftActivity?: Maybe<NftActivityConnection>;
nftAssets?: Maybe<NftAssetConnection>;
nftBalances?: Maybe<NftBalanceConnection>;
nftCollections?: Maybe<NftCollectionConnection>;
@@ -613,17 +656,25 @@ export type Query = {
token?: Maybe<Token>;
tokenProjects?: Maybe<Array<Maybe<TokenProject>>>;
tokens?: Maybe<Array<Maybe<Token>>>;
topCollections?: Maybe<NftCollectionConnection>;
topTokens?: Maybe<Array<Maybe<Token>>>;
};
export type QueryNftActivityArgs = {
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<NftActivityFilterInput>;
limit?: InputMaybe<Scalars['Int']>;
};
export type QueryNftAssetsArgs = {
address: Scalars['String'];
after?: InputMaybe<Scalars['String']>;
asc?: InputMaybe<Scalars['Boolean']>;
before?: InputMaybe<Scalars['String']>;
chain?: InputMaybe<Chain>;
datasource?: InputMaybe<DatasourceProvider>;
filter?: InputMaybe<NftAssetsFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
@@ -636,7 +687,6 @@ export type QueryNftBalancesArgs = {
before?: InputMaybe<Scalars['String']>;
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
datasource?: InputMaybe<DatasourceProvider>;
filter?: InputMaybe<NftBalancesFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
@@ -646,12 +696,10 @@ export type QueryNftBalancesArgs = {
export type QueryNftCollectionsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
datasource?: InputMaybe<DatasourceProvider>;
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<NftCollectionsFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
limit?: InputMaybe<Scalars['Int']>;
};
@@ -670,7 +718,6 @@ export type QueryNftRouteArgs = {
export type QueryPortfoliosArgs = {
ownerAddresses: Array<Scalars['String']>;
useAltDataSource?: InputMaybe<Scalars['Boolean']>;
};
@@ -700,6 +747,15 @@ export type QueryTokensArgs = {
};
export type QueryTopCollectionsArgs = {
chains?: InputMaybe<Array<Chain>>;
cursor?: InputMaybe<Scalars['String']>;
duration?: InputMaybe<HistoryDuration>;
limit?: InputMaybe<Scalars['Int']>;
orderBy?: InputMaybe<CollectionSortableField>;
};
export type QueryTopTokensArgs = {
chain?: InputMaybe<Chain>;
orderBy?: InputMaybe<TokenSortableField>;

View File

@@ -112,7 +112,7 @@ export function useSearchInactiveTokenLists(search: string | undefined, minResul
const result: WrappedTokenInfo[] = []
const addressSet: { [address: string]: true } = {}
for (const url of inactiveUrls) {
const list = lists[url].current
const list = lists[url]?.current
if (!list) continue
for (const tokenInfo of list.tokens) {
if (tokenInfo.chainId === chainId && tokenFilter(tokenInfo)) {

View File

@@ -1,18 +1,16 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
import { BagFooter } from 'nft/components/bag/BagFooter'
import ListingModal from 'nft/components/bag/profile/ListingModal'
import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal'
import { Column } from 'nft/components/Flex'
import { Overlay } from 'nft/components/modals/Overlay'
import { buttonTextMedium, commonButtonStyles } from 'nft/css/common.css'
import {
useBag,
useIsMobile,
@@ -66,7 +64,7 @@ const BagContainer = styled.div<{ raiseZIndex: boolean; isProfilePage: boolean }
border-radius: 16px;
box-shadow: ${({ theme }) => theme.shallowShadow};
z-index: ${({ raiseZIndex, isProfilePage }) =>
raiseZIndex ? (isProfilePage ? Z_INDEX.modalOverTooltip : Z_INDEX.modalBackdrop + 2) : 3};
raiseZIndex ? (isProfilePage ? Z_INDEX.modalOverTooltip : Z_INDEX.modalBackdrop - 1) : 3};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
right: 0px;
@@ -90,6 +88,24 @@ const DetailsPageBackground = styled.div`
height: 100%;
`
const ContinueButton = styled.div`
background: ${({ theme }) => theme.accentAction};
color: ${({ theme }) => theme.accentTextLightPrimary};
margin: 32px 28px 16px;
padding: 10px 0px;
border-radius: 12px;
text-align: center;
font-size: 16px;
font-weight: 600;
line-height: 20px;
cursor: pointer;
transition: ${({ theme }) => theme.transition.duration.medium};
:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
`
const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
<Box
marginX="24"
@@ -114,10 +130,7 @@ const Bag = () => {
shallow
)
const { profilePageState, setProfilePageState } = useProfilePageState(
({ setProfilePageState, state }) => ({ profilePageState: state, setProfilePageState }),
shallow
)
const { setProfilePageState } = useProfilePageState(({ setProfilePageState }) => ({ setProfilePageState }))
const {
bagStatus,
@@ -139,7 +152,6 @@ const Bag = () => {
const isDetailsPage = useIsNftDetailsPage()
const isNFTPage = useIsNftPage()
const isMobile = useIsMobile()
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const usingGqlRouting = useGqlRoutingFlag() === GqlRoutingVariant.Enabled
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
@@ -398,48 +410,34 @@ const Bag = () => {
return (
<Portal>
<BagContainer data-testid="nft-bag" raiseZIndex={isMobile || isModalOpen} isProfilePage={isProfilePage}>
{!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? (
<>
<BagHeader
numberOfAssets={isProfilePage ? sellAssets.length : itemsInBag.length}
closeBag={handleCloseBag}
resetFlow={isProfilePage ? resetSellAssets : reset}
isProfilePage={isProfilePage}
/>
{shouldRenderEmptyState && <EmptyState />}
<ScrollingIndicator top show={userCanScroll && scrollProgress > 0} />
<Column ref={scrollRef} className={styles.assetsContainer} onScroll={scrollHandler} gap="12">
{isProfilePage ? <ProfileBagContent /> : <BagContent />}
</Column>
{hasAssetsToShow && !isProfilePage && (
<BagFooter totalEthPrice={totalEthPrice} fetchAssets={fetchAssets} eventProperties={eventProperties} />
)}
{isSellingAssets && isProfilePage && (
<Box
marginTop="32"
marginX="28"
marginBottom="16"
paddingY="10"
className={`${buttonTextMedium} ${commonButtonStyles}`}
backgroundColor="accentAction"
color="white"
textAlign="center"
onClick={() => {
;(isMobile || isNftListV2) && toggleBag()
setProfilePageState(ProfilePageStateType.LISTING)
sendAnalyticsEvent(NFTEventName.NFT_PROFILE_PAGE_START_SELL, {
list_quantity: sellAssets.length,
collection_addresses: sellAssets.map((asset) => asset.asset_contract.address),
token_ids: sellAssets.map((asset) => asset.tokenId),
})
}}
>
Continue
</Box>
)}
</>
) : (
<ListingModal />
<BagHeader
numberOfAssets={isProfilePage ? sellAssets.length : itemsInBag.length}
closeBag={handleCloseBag}
resetFlow={isProfilePage ? resetSellAssets : reset}
isProfilePage={isProfilePage}
/>
{shouldRenderEmptyState && <EmptyState />}
<ScrollingIndicator top show={userCanScroll && scrollProgress > 0} />
<Column ref={scrollRef} className={styles.assetsContainer} onScroll={scrollHandler} gap="12">
{isProfilePage ? <ProfileBagContent /> : <BagContent />}
</Column>
{hasAssetsToShow && !isProfilePage && (
<BagFooter totalEthPrice={totalEthPrice} fetchAssets={fetchAssets} eventProperties={eventProperties} />
)}
{isSellingAssets && isProfilePage && (
<ContinueButton
onClick={() => {
toggleBag()
setProfilePageState(ProfilePageStateType.LISTING)
sendAnalyticsEvent(NFTEventName.NFT_PROFILE_PAGE_START_SELL, {
list_quantity: sellAssets.length,
collection_addresses: sellAssets.map((asset) => asset.asset_contract.address),
token_ids: sellAssets.map((asset) => asset.tokenId),
})
}}
>
<Trans>Continue</Trans>
</ContinueButton>
)}
</BagContainer>

View File

@@ -1,333 +0,0 @@
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
showWarningOverride?: boolean
}
export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => {
const {
addMarketplaceWarning,
sellAssets,
removeAllMarketplaceWarnings,
showResolveIssues,
toggleShowResolveIssues,
issues,
setIssues,
} = useSellAsset(
({
addMarketplaceWarning,
sellAssets,
removeAllMarketplaceWarnings,
showResolveIssues,
toggleShowResolveIssues,
issues,
setIssues,
}) => ({
addMarketplaceWarning,
sellAssets,
removeAllMarketplaceWarnings,
showResolveIssues,
toggleShowResolveIssues,
issues,
setIssues,
}),
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 theme = useTheme()
const warningRef = useRef<HTMLDivElement>(null)
useOnClickOutside(warningRef, () => {
!isNftListV2 && setShowWarning(false)
})
useEffect(() => {
const [newCollectionsToApprove, newListings] = getListings(sellAssets)
setListings(newListings)
setCollectionsRequiringApproval(newCollectionsToApprove)
setListingStatus(ListingStatus.DEFINED)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sellAssets])
const [
noMarketplacesSelected,
missingExpiration,
invalidExpiration,
overMaxExpiration,
listingsMissingPrice,
listingsBelowFloor,
listingsAboveSellOrderFloor,
invalidPrices,
] = useMemo(() => {
const noMarketplacesSelected = sellAssets.some((asset: WalletAsset) => asset.marketplaces === undefined)
const missingExpiration = sellAssets.some((asset) => {
return (
asset.expirationTime != null &&
(isNaN(asset.expirationTime) || asset.expirationTime * 1000 - Date.now() < ms`60 seconds`)
)
})
const invalidExpiration = sellAssets.some((asset) => {
return asset.expirationTime != null && isNaN(asset.expirationTime)
})
const overMaxExpiration = sellAssets.some((asset) => {
return asset.expirationTime != null && asset.expirationTime - Date.now() > ms`180 days`
})
const listingsMissingPrice: [WalletAsset, Listing][] = []
const listingsBelowFloor: [WalletAsset, Listing][] = []
const listingsAboveSellOrderFloor: [WalletAsset, Listing][] = []
const invalidPrices: [WalletAsset, Listing][] = []
for (const asset of sellAssets) {
if (asset.newListings) {
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) * BELOW_FLOOR_PRICE_THRESHOLD &&
!listing.overrideFloorPrice
)
listingsBelowFloor.push([asset, listing])
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 [
noMarketplacesSelected,
missingExpiration,
invalidExpiration,
overMaxExpiration,
listingsMissingPrice,
listingsBelowFloor,
listingsAboveSellOrderFloor,
invalidPrices,
]
}, [isNftListV2, sellAssets, setIssues, showResolveIssues, toggleShowResolveIssues])
const [disableListButton, warningMessage] = useMemo(() => {
const disableListButton =
noMarketplacesSelected ||
missingExpiration ||
invalidExpiration ||
overMaxExpiration ||
invalidPrices.length > 0 ||
listingsMissingPrice.length > 0
const warningMessage = noMarketplacesSelected
? 'No marketplaces selected'
: missingExpiration
? 'Set duration'
: invalidExpiration
? 'Invalid duration'
: overMaxExpiration
? 'Max duration is 6 months'
: listingsMissingPrice.length > 0
? `${listingsMissingPrice.length} item price${pluralize(listingsMissingPrice.length)} not set`
: invalidPrices.length > 0
? `${invalidPrices.length} price${pluralize(invalidPrices.length)} are invalid`
: listingsBelowFloor.length > 0
? `${listingsBelowFloor.length} item${pluralize(listingsBelowFloor.length)} listed below floor`
: listingsAboveSellOrderFloor.length > 0
? `${listingsAboveSellOrderFloor.length} item${pluralize(listingsAboveSellOrderFloor.length)} already listed`
: ''
return [disableListButton, warningMessage]
}, [
noMarketplacesSelected,
missingExpiration,
invalidExpiration,
overMaxExpiration,
listingsMissingPrice,
invalidPrices,
listingsBelowFloor,
listingsAboveSellOrderFloor,
])
useEffect(() => {
setShowWarning(false)
}, [warningMessage])
const addWarningMessages = () => {
removeAllMarketplaceWarnings()
if (!missingExpiration && !noMarketplacesSelected) {
if (listingsMissingPrice.length > 0) {
for (const [asset, listing] of listingsMissingPrice) {
addMarketplaceWarning(asset, {
message: 'PLEASE SET A PRICE',
marketplace: listing.marketplace,
})
}
} else if (invalidPrices.length > 0) {
for (const [asset, listing] of invalidPrices) {
!listing.overrideFloorPrice &&
addMarketplaceWarning(asset, {
message: `INVALID PRICE`,
marketplace: listing.marketplace,
})
}
}
}
setShowWarning(true)
}
const warningWrappedClick = () => {
if ((!disableListButton && canContinue) || showWarningOverride) {
if (issues && isNftListV2) !showResolveIssues && toggleShowResolveIssues()
else if (listingsBelowFloor.length) setShowWarning(true)
else onClick()
} else addWarningMessages()
}
return (
<>
<Box position="relative">
{!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>
) : (
<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={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>
{showWarning && (
<BelowFloorWarningModal
listingsBelowFloor={listingsBelowFloor}
closeModal={() => setShowWarning(false)}
startListing={onClick}
/>
)}
</>
)
}

View File

@@ -1,79 +0,0 @@
import { style } from '@vanilla-extract/css'
import { sprinkles } from 'nft/css/sprinkles.css'
export const chevron = style([
sprinkles({
height: '28',
width: '28',
transition: '250',
marginLeft: 'auto',
marginRight: '0',
}),
])
export const chevronDown = style({
transform: 'rotate(180deg)',
cursor: 'pointer',
})
export const sectionDivider = style([
sprinkles({
borderRadius: '20',
marginTop: '8',
width: 'full',
borderWidth: '0.5px',
borderStyle: 'solid',
borderColor: 'backgroundOutline',
}),
])
export const button = style([
sprinkles({
paddingX: { sm: '12', md: '16' },
paddingY: { sm: '10', md: '16' },
textAlign: 'center',
fontWeight: 'semibold',
fontSize: { sm: '16', md: '20' },
lineHeight: { sm: '20', md: '24' },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'flex-end',
borderRadius: '12',
}),
])
export const listingModalIcon = style([
sprinkles({
borderWidth: '1px',
borderStyle: 'solid',
borderColor: 'backgroundSurface',
}),
{
boxSizing: 'border-box',
marginLeft: '-2px',
marginRight: '4px',
},
])
export const warningTooltip = style([
sprinkles({
paddingTop: '8',
paddingRight: '8',
paddingBottom: '8',
paddingLeft: '12',
}),
{
boxShadow: '0px 4px 16px rgba(10, 10, 59, 0.2)',
},
])
export const listingSectionBorder = style([
sprinkles({
padding: '8',
borderRadius: '8',
borderColor: 'backgroundOutline',
borderStyle: 'solid',
borderWidth: '1px',
}),
])

View File

@@ -1,322 +0,0 @@
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { ChevronLeftIcon, XMarkIcon } from 'nft/components/icons'
import { caption, headlineSmall, subhead, subheadSmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useNFTList, useSellAsset } from 'nft/hooks'
import { logListing, looksRareNonceFetcher } from 'nft/queries'
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'
import { ListingSection } from './ListingSection'
import { approveCollectionRow, getTotalEthValue, pauseRow, resetRow, signListingRow, verifyStatus } from './utils'
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 [openIndex, setOpenIndex] = useState(0)
const [allCollectionsApproved, setAllCollectionsApproved] = useState(false)
const toggleCart = useBag((state) => state.toggleBag)
const looksRareNonceRef = useRef(looksRareNonce)
const isMobile = useIsMobile()
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
useEffect(() => {
useNFTList.subscribe((state) => (looksRareNonceRef.current = state.looksRareNonce))
}, [])
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
useEffect(() => {
fetchPrice().then((price) => {
setEthPriceInUSD(price || 0)
})
}, [])
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,
}
// when all collections have been approved, auto start the signing process
useEffect(() => {
collectionsRequiringApproval?.length &&
setAllCollectionsApproved(
collectionsRequiringApproval.every((collection: CollectionRow) => collection.status === ListingStatus.APPROVED)
)
if (
allCollectionsApproved &&
(listingStatus === ListingStatus.PENDING ||
listingStatus === ListingStatus.CONTINUE ||
listingStatus === ListingStatus.SIGNING)
) {
resetAllRows()
signListings()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [collectionsRequiringApproval, allCollectionsApproved])
const allCollectionsApprovedOrPaused = useMemo(
() =>
collectionsRequiringApproval.every(
(collection: CollectionRow) =>
collection.status === ListingStatus.APPROVED || collection.status === ListingStatus.PAUSED
),
[collectionsRequiringApproval]
)
const allListingsApprovedOrPaused = useMemo(
() =>
listings.every(
(listing: ListingRow) => listing.status === ListingStatus.APPROVED || listing.status === ListingStatus.PAUSED
),
[listings]
)
// go back to a ready state after a successful retry
useEffect(() => {
if (listingStatus === ListingStatus.SIGNING && allCollectionsApprovedOrPaused && allListingsApprovedOrPaused) {
resetAllRows()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allCollectionsApprovedOrPaused, allListingsApprovedOrPaused])
// handles the modal wide listing state based on conglomeration of the wallet, collection, and listing states
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)
if (!collectionsRequiringApproval?.some((collection) => collection.status === ListingStatus.PAUSED)) {
setListingStatus(ListingStatus.SIGNING)
setOpenIndex(1)
}
// for all unique collection, marketplace combos -> approve collections
for (const collectionRow of collectionsRequiringApproval) {
verifyStatus(collectionRow.status) &&
(isMobile
? await approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows)
: approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows))
}
}
const signListings = async () => {
if (!signer || !provider) return
setListingStatus(ListingStatus.SIGNING)
setOpenIndex(2)
// sign listings
for (const listing of listings) {
verifyStatus(listing.status) &&
(await signListingRow(
listing,
signer,
provider,
getLooksRareNonce,
setLooksRareNonce,
setListingStatusAndCallback,
pauseAllRows
))
}
const allListingsSigned = listings.every((listing: ListingRow) => listing.status === ListingStatus.APPROVED)
const paused = listings.some((listing: ListingRow) => listing.status === ListingStatus.PAUSED)
if (allListingsSigned) {
setOpenIndex(0)
setListingStatus(ListingStatus.APPROVED)
} else if (!paused) {
setListingStatus(ListingStatus.FAILED)
}
sendAnalyticsEvent(NFTEventName.NFT_LISTING_COMPLETED, {
signatures_approved: listings.filter((asset) => asset.status === ListingStatus.APPROVED),
list_quantity: listings.length,
usd_value: ethPriceInUSD * totalEthListingValue,
...trace,
})
await logListing(listings, (await signer?.getAddress()) ?? '')
}
const pauseAllRows = () => {
for (const collection of collectionsRequiringApproval) {
pauseRow(collection, collectionsRequiringApproval, setCollectionsRequiringApproval as Dispatch<AssetRow[]>)
}
for (const listing of listings) {
pauseRow(listing, listings, setListings as Dispatch<AssetRow[]>)
}
}
const resetAllRows = () => {
for (const collection of collectionsRequiringApproval) {
resetRow(collection, collectionsRequiringApproval, setCollectionsRequiringApproval as Dispatch<AssetRow[]>)
}
for (const listing of listings) {
resetRow(listing, listings, setListings as Dispatch<AssetRow[]>)
}
}
const clickStopListing = () => {
pauseAllRows()
}
const clickStartListingFlow = () => {
resetAllRows()
allCollectionsApproved ? signListings() : startListingFlow()
}
const showSuccessScreen = useMemo(() => listingStatus === ListingStatus.APPROVED, [listingStatus])
return (
<Trace modal={InterfaceModalName.NFT_LISTING}>
<Column paddingTop="20" paddingBottom="20" paddingLeft="12" paddingRight="12">
<Row className={headlineSmall} marginBottom="10">
{isMobile && !showSuccessScreen && (
<Box paddingTop="4" marginRight="4" onClick={toggleCart}>
<ChevronLeftIcon height={28} width={28} />
</Box>
)}
{showSuccessScreen ? 'Success!' : `Listing ${sellAssets.length} NFTs`}
<Box
as="button"
border="none"
color="textSecondary"
backgroundColor="backgroundSurface"
marginLeft="auto"
marginRight="0"
paddingRight="0"
display={{ sm: 'flex', md: 'none' }}
cursor="pointer"
onClick={toggleCart}
>
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
</Box>
</Row>
<Column overflowX="hidden" overflowY="auto" style={{ maxHeight: '60vh' }}>
{showSuccessScreen ? (
<Trace
name={NFTEventName.NFT_LISTING_COMPLETED}
properties={{ list_quantity: listings.length, usd_value: ethPriceInUSD * totalEthListingValue, ...trace }}
shouldLogImpression
>
<ListingSection
sectionTitle={`Listed ${listings.length} item${pluralize(listings.length)} for sale`}
rows={listings}
index={0}
openIndex={openIndex}
isSuccessScreen={true}
/>
</Trace>
) : (
<>
<ListingSection
sectionTitle={`Approve ${collectionsRequiringApproval.length} collection${pluralize(
collectionsRequiringApproval.length
)}`}
title="COLLECTIONS"
rows={collectionsRequiringApproval}
index={1}
openIndex={openIndex}
/>
<ListingSection
sectionTitle={`Confirm ${listings.length} listing${pluralize(listings.length)}`}
caption="Now you can sign to list each item"
title="NFTS"
rows={listings}
index={2}
openIndex={openIndex}
/>
</>
)}
</Column>
<hr className={styles.sectionDivider} />
<Row className={subhead} marginTop="12" marginBottom={showSuccessScreen ? '8' : '20'}>
Return if sold
<Row className={subheadSmall} marginLeft="auto" marginRight="0">
{totalEthListingValue}
&nbsp;ETH
</Row>
</Row>
{showSuccessScreen ? (
<Box as="span" className={caption} color="textSecondary">
Status:{' '}
<Box as="span" color="accentSuccess">
Confirmed
</Box>
</Box>
) : (
<ListingButton onClick={clickStartListingFlow} buttonText="Start listing" showWarningOverride={isMobile} />
)}
{(listingStatus === ListingStatus.PENDING || listingStatus === ListingStatus.SIGNING) && (
<Box
as="button"
border="none"
backgroundColor="backgroundSurface"
cursor="pointer"
color="orange"
className={styles.button}
onClick={clickStopListing}
type="button"
>
Stop listing
</Box>
)}
</Column>
</Trace>
)
}
export default ListingModal

View File

@@ -1,187 +0,0 @@
import clsx from 'clsx'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { ApprovedCheckmarkIcon, ChevronUpIcon, FailedListingIcon, LoadingIcon } from 'nft/components/icons'
import { badge, bodySmall, buttonTextSmall, subhead } from 'nft/css/common.css'
import { useSellAsset } from 'nft/hooks'
import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
import { formatEthPrice, numberToWei } from 'nft/utils/currency'
import { useEffect, useState } from 'react'
import * as styles from './ListingModal.css'
export const ListingSection = ({
sectionTitle,
caption = undefined,
title = undefined,
rows,
index,
openIndex,
isSuccessScreen = false,
}: {
sectionTitle: string
caption?: string
title?: string
rows: AssetRow[]
index: number
openIndex: number
isSuccessScreen?: boolean
}) => {
const [isOpen, setIsOpen] = useState(false)
const notAllApproved = rows.some((row: AssetRow) => row.status !== ListingStatus.APPROVED)
const sellAssets = useSellAsset((state) => state.sellAssets)
const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace)
const removeRow = (row: any) => {
// collections
if (index === 1) {
for (const asset of sellAssets)
if (asset.asset_contract.address === row.collectionAddress) removeAssetMarketplace(asset, row.marketplace)
}
// listings
else removeAssetMarketplace(row.asset, row.marketplace)
}
useEffect(() => {
setIsOpen(index === openIndex)
}, [index, openIndex])
function getListingRowPrice(row: AssetRow): number | undefined {
const listingRow = row as ListingRow
const newListings = listingRow.asset.newListings
return newListings?.find((listing) => listing.marketplace.name === listingRow.marketplace.name)?.price ?? 0
}
const allApproved = !notAllApproved && rows.length > 0 && !isSuccessScreen
return (
<Row
flexWrap="wrap"
className={subhead}
marginTop="10"
marginBottom="10"
onClick={() => rows.length > 0 && setIsOpen(!isOpen)}
color={allApproved ? 'accentSuccess' : 'textPrimary'}
>
{allApproved && <ApprovedCheckmarkIcon style={{ marginRight: '8px' }} />}
{sectionTitle}
{!isSuccessScreen && <ChevronUpIcon className={clsx(`${isOpen ? '' : styles.chevronDown} ${styles.chevron}`)} />}
{(isOpen || isSuccessScreen) && (
<Column
gap="12"
width="full"
paddingTop={isSuccessScreen ? '28' : 'auto'}
className={clsx(!isSuccessScreen && styles.listingSectionBorder)}
>
{caption && (
<Box color="textPrimary" fontWeight="normal" className={caption}>
{caption}
</Box>
)}
{title && (
<Box color="textSecondary" className={badge}>
{title}
</Box>
)}
<Column gap="8">
{rows.map((row: AssetRow, index) => {
return (
<Column key={index} gap="8">
<Row>
{row.images?.map((image, index) => {
return (
<Box
as="img"
height="20"
width="20"
borderRadius={index === 0 && (row as CollectionRow).collectionAddress ? 'round' : '4'}
style={{ zIndex: 2 - index }}
className={styles.listingModalIcon}
src={image}
alt={row.name}
key={index}
/>
)
})}
<Box
marginLeft="8"
marginRight="auto"
fontWeight="normal"
color="textPrimary"
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="nowrap"
maxWidth={{
sm: 'max',
md:
row.status === ListingStatus.REJECTED || row.status === ListingStatus.FAILED ? '120' : 'full',
}}
className={bodySmall}
>
{row.name}
</Box>
{isSuccessScreen ? (
getListingRowPrice(row) &&
`${formatEthPrice(numberToWei(getListingRowPrice(row) ?? 0).toString())} ETH`
) : row.status === ListingStatus.APPROVED ? (
<ApprovedCheckmarkIcon height="20" width="20" />
) : row.status === ListingStatus.FAILED || row.status === ListingStatus.REJECTED ? (
<Row gap="4">
<Box fontWeight="normal" fontSize="14" color="textSecondary">
{row.status}
</Box>
<FailedListingIcon />
</Row>
) : (
row.status === ListingStatus.SIGNING && <LoadingIcon height="20" width="20" stroke="#4673FA" />
)}
</Row>
{(row.status === ListingStatus.FAILED || row.status === ListingStatus.REJECTED) && (
<Row gap="8" justifyContent="center">
<Box
width="120"
as="button"
className={buttonTextSmall}
borderRadius="12"
border="none"
color="red400"
height="32"
cursor="pointer"
style={{ backgroundColor: '#FA2B391A' }}
onClick={async (e) => {
e.stopPropagation()
removeRow(row)
}}
>
Remove
</Box>
<Box
width="120"
as="button"
className={buttonTextSmall}
borderRadius="12"
border="none"
color="accentAction"
height="32"
cursor="pointer"
style={{ backgroundColor: '#4C82FB29' }}
onClick={async (e) => {
e.stopPropagation()
if (row.callback) {
await row.callback()
}
}}
>
Try again
</Box>
</Row>
)}
</Column>
)
})}
</Column>
</Column>
)}
</Row>
)
}

View File

@@ -1,237 +0,0 @@
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'
import { approveCollection, LOOKS_RARE_CREATOR_BASIS_POINTS, signListing } from 'nft/utils/listNfts'
import { Dispatch } from 'react'
const updateStatus = ({
listing,
newStatus,
rows,
setRows,
callback,
}: {
listing: AssetRow
newStatus: ListingStatus
rows: AssetRow[]
setRows: Dispatch<AssetRow[]>
callback?: () => Promise<void>
}) => {
const rowsCopy = [...rows]
const index = rows.findIndex((n) => n === listing)
listing.status = newStatus
if (callback) listing.callback = callback
rowsCopy[index] = listing
setRows(rowsCopy)
}
export async function approveCollectionRow(
collectionRow: CollectionRow,
signer: JsonRpcSigner,
setCollectionStatusAndCallback: (
collection: CollectionRow,
status: ListingStatus,
callback?: () => Promise<void>
) => void,
pauseAllRows?: () => void
) {
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
: marketplace.name === 'Rarible'
? LOOKSRARE_MARKETPLACE_CONTRACT
: marketplace.name === 'X2Y2'
? X2Y2_TRANSFER_CONTRACT
: addresses.TRANSFER_MANAGER_ERC721
!!collectionAddress &&
(await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
setCollectionStatusAndCallback(collectionRow, newStatus, callback)
))
if (
(collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) &&
pauseAllRows
)
pauseAllRows()
}
export async function signListingRow(
listing: ListingRow,
signer: JsonRpcSigner,
provider: Web3Provider,
getLooksRareNonce: () => number,
setLooksRareNonce: (nonce: number) => void,
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void,
pauseAllRows?: () => void
) {
const looksRareNonce = getLooksRareNonce()
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) =>
setListingStatusAndCallback(listing, newStatus, callback)
)
if (listing.status === ListingStatus.REJECTED && pauseAllRows) {
pauseAllRows()
} else {
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
}
}
export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
const total = sellAssets.reduce((total, asset: WalletAsset) => {
if (asset.newListings?.length) {
const maxListing = asset.newListings.reduce((a, b) => ((a.price ?? 0) > (b.price ?? 0) ? a : b))
// LooksRare is a unique case where creator royalties are a flat 0.5% or 50 basis points
const maxFee =
maxListing.marketplace.fee +
(maxListing.marketplace.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset?.basisPoints ?? 0) / 100
return total + (maxListing.price ?? 0) - (maxListing.price ?? 0) * (maxFee / 100)
}
return total
}, 0)
return total ? Math.round(total * 10000 + Number.EPSILON) / 10000 : 0
}
export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]] => {
const newCollectionsToApprove: CollectionRow[] = []
const newListings: ListingRow[] = []
sellAssets.forEach((asset) => {
asset.marketplaces?.forEach((marketplace: ListingMarket) => {
const newListing = {
images: [asset.smallImageUrl, marketplace.icon],
name: asset.name || `#${asset.tokenId}`,
status: ListingStatus.DEFINED,
asset,
marketplace,
price: asset.newListings?.find((listing) => listing.marketplace.name === marketplace.name)?.price,
}
newListings.push(newListing)
if (
!newCollectionsToApprove.some(
(collectionRow: CollectionRow) =>
collectionRow.collectionAddress === asset.asset_contract.address &&
collectionRow.marketplace.name === marketplace.name
)
) {
const newCollectionRow = {
images: [asset.asset_contract.image_url, marketplace.icon],
name: asset.asset_contract.name,
status: ListingStatus.DEFINED,
collectionAddress: asset.asset_contract.address,
isVerified: asset.collectionIsVerified,
marketplace,
}
newCollectionsToApprove.push(newCollectionRow)
}
})
})
return [newCollectionsToApprove, newListings]
}
type ListingState = {
allListingsPending: boolean
allListingsDefined: boolean
allListingsApproved: boolean
allCollectionsPending: boolean
allCollectionsDefined: boolean
anyActiveSigning: boolean
anyActiveFailures: boolean
anyActiveRejections: boolean
anyPaused: boolean
}
export const getListingState = (
collectionsRequiringApproval: CollectionRow[],
listings: ListingRow[]
): ListingState => {
let allListingsPending = true
let allListingsDefined = true
let allListingsApproved = true
let allCollectionsPending = true
let allCollectionsDefined = true
let anyActiveSigning = false
let anyActiveFailures = false
let anyActiveRejections = false
let anyPaused = false
if (collectionsRequiringApproval.length === 0) {
allCollectionsDefined = allCollectionsPending = false
}
for (const collection of collectionsRequiringApproval) {
if (collection.status !== ListingStatus.PENDING) allCollectionsPending = false
if (collection.status !== ListingStatus.DEFINED) allCollectionsDefined = false
if (collection.status === ListingStatus.SIGNING) anyActiveSigning = true
else if (collection.status === ListingStatus.FAILED) anyActiveFailures = true
else if (collection.status === ListingStatus.REJECTED) anyActiveRejections = true
else if (collection.status === ListingStatus.PAUSED) anyPaused = true
}
if (listings.length === 0) {
allListingsApproved = allListingsDefined = allListingsPending = false
}
for (const listing of listings) {
if (listing.status !== ListingStatus.PENDING) allListingsPending = false
if (listing.status !== ListingStatus.DEFINED) allListingsDefined = false
if (listing.status !== ListingStatus.APPROVED) allListingsApproved = false
if (listing.status === ListingStatus.SIGNING) anyActiveSigning = true
else if (listing.status === ListingStatus.FAILED) anyActiveFailures = true
else if (listing.status === ListingStatus.REJECTED) anyActiveRejections = true
else if (listing.status === ListingStatus.PAUSED) anyPaused = true
}
return {
allListingsPending,
allListingsDefined,
allListingsApproved,
allCollectionsPending,
allCollectionsDefined,
anyActiveSigning,
anyActiveFailures,
anyActiveRejections,
anyPaused,
}
}
export const verifyStatus = (status: ListingStatus) => {
return status !== ListingStatus.PAUSED && status !== ListingStatus.APPROVED
}
export const pauseRow = (row: AssetRow, rows: AssetRow[], setRows: Dispatch<AssetRow[]>) => {
if (row.status === ListingStatus.PENDING || row.status === ListingStatus.DEFINED)
updateStatus({
listing: row,
newStatus: ListingStatus.PAUSED,
rows,
setRows,
})
}
export const resetRow = (row: AssetRow, rows: AssetRow[], setRows: Dispatch<AssetRow[]>) => {
if (
row.status === ListingStatus.PAUSED ||
row.status === ListingStatus.FAILED ||
row.status === ListingStatus.REJECTED
)
updateStatus({
listing: row,
newStatus: ListingStatus.DEFINED,
rows,
setRows,
})
}

View File

@@ -1,114 +0,0 @@
import { style } from '@vanilla-extract/css'
import { calc } from '@vanilla-extract/css-utils'
import { sprinkles, themeVars, vars } from 'nft/css/sprinkles.css'
export const card = style([
sprinkles({
overflow: 'hidden',
paddingBottom: '12',
borderRadius: '16',
}),
{
boxSizing: 'border-box',
WebkitBoxSizing: 'border-box',
boxShadow: vars.color.cardDropShadow,
backgroundColor: themeVars.colors.backgroundSurface,
':after': {
content: '',
position: 'absolute',
top: '0px',
right: ' 0px',
bottom: ' 0px',
left: '0px',
border: ' 1px solid',
borderRadius: '16px',
borderColor: '#5D678524',
pointerEvents: 'none',
},
},
])
export const loadingBackground = style({
background: `linear-gradient(270deg, ${themeVars.colors.backgroundOutline} 0%, ${themeVars.colors.backgroundSurface} 100%)`,
})
export const cardImageHover = style({
transform: 'scale(1.15)',
})
export const selectedCard = style([
card,
sprinkles({
background: 'backgroundSurface',
}),
{
':after': {
border: '2px solid',
borderColor: vars.color.accentAction,
},
},
])
export const button = style([
sprinkles({
display: 'flex',
width: 'full',
position: 'relative',
paddingY: '8',
marginTop: { sm: '8', md: '10' },
marginBottom: '12',
borderRadius: '12',
border: 'none',
justifyContent: 'center',
transition: '250',
cursor: 'pointer',
}),
{
lineHeight: '16px',
},
])
export const marketplaceIcon = style([
sprinkles({
display: 'inline-block',
width: '16',
height: '16',
borderRadius: '4',
flexShrink: '0',
marginLeft: '8',
}),
{
verticalAlign: 'top',
},
])
export const rarityInfo = style([
sprinkles({
display: 'flex',
borderRadius: '4',
height: '16',
color: 'textPrimary',
background: 'backgroundInteractive',
fontSize: '10',
fontWeight: 'semibold',
paddingX: '4',
}),
{
lineHeight: '12px',
letterSpacing: '0.04em',
backdropFilter: 'blur(6px)',
},
])
export const playbackSwitch = style([
sprinkles({
position: 'absolute',
width: '40',
height: '40',
zIndex: '1',
}),
{
marginLeft: calc.subtract('100%', '50px'),
transform: 'translateY(-56px)',
},
])

View File

@@ -1,10 +1,10 @@
import { BigNumber } from '@ethersproject/bignumber'
import clsx from 'clsx'
import { t, Trans } from '@lingui/macro'
import Column from 'components/Column'
import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import {
MinusIconLarge,
PauseButtonIcon,
@@ -14,8 +14,6 @@ import {
RarityVerifiedIcon,
VerifiedIcon,
} from 'nft/components/icons'
import { body, bodySmall, buttonTextMedium, subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useIsMobile } from 'nft/hooks'
import { GenieAsset, Rarity, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types'
import { fallbackProvider, isAudio, isVideo, putCommas } from 'nft/utils'
@@ -32,11 +30,11 @@ import {
useRef,
useState,
} from 'react'
import { AlertTriangle } from 'react-feather'
import { AlertTriangle, Pause, Play } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import * as styles from './Card.css'
import { colors } from 'theme/colors'
import { opacify } from 'theme/utils'
/* -------- ASSET CONTEXT -------- */
export interface CardContextProps {
@@ -166,6 +164,31 @@ const StyledImageContainer = styled.div<{ isDisabled?: boolean }>`
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
`
const CardContainer = styled.div<{ selected: boolean }>`
position: relative;
border-radius: ${BORDER_RADIUS}px;
background-color: ${({ theme }) => theme.backgroundSurface};
overflow: hidden;
padding-bottom: 12px;
border-radius: 16px;
box-shadow: rgba(0, 0, 0, 10%) 0px 4px 12px;
box-sizing: border-box;
-webkit-box-sizing: border-box;
:after {
content: '';
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
border: ${({ selected }) => (selected ? '2px' : '1px')} solid;
border-radius: 16px;
border-color: ${({ theme, selected }) => (selected ? theme.accentAction : opacify(12, colors.gray500))};
pointer-events: none;
}
`
/* -------- ASSET CARD -------- */
interface CardProps {
asset: GenieAsset | WalletAsset
@@ -220,19 +243,16 @@ const Container = ({
return (
<CardContext.Provider value={providerValue}>
<Box
position="relative"
<CardContainer
selected={selected}
ref={assetRef}
borderRadius={BORDER_RADIUS}
className={selected ? styles.selectedCard : styles.card}
draggable={false}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
transition="250"
onClick={isDisabled ? () => null : onClick ?? handleAssetInBag}
>
{children}
</Box>
</CardContainer>
</CardContext.Provider>
)
}
@@ -277,6 +297,13 @@ function getHeightFromAspectRatio(uniformAspectRatio: UniformAspectRatio, render
: renderedHeight
}
function getMediaAspectRatio(
uniformAspectRatio?: UniformAspectRatio,
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
): string {
return uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
}
interface ImageProps {
uniformAspectRatio?: UniformAspectRatio
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
@@ -284,6 +311,29 @@ interface ImageProps {
setRenderedHeight?: (renderedHeight: number | undefined) => void
}
const StyledMediaContainer = styled(Row)`
overflow: hidden;
border-top-left-radius: ${BORDER_RADIUS}px;
border-top-right-radius: ${BORDER_RADIUS}px;
`
const StyledImage = styled.img<{
hovered: boolean
imageLoading: boolean
$aspectRatio?: string
$hidden?: boolean
}>`
width: 100%;
aspect-ratio: ${({ $aspectRatio }) => $aspectRatio};
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} transform`};
will-change: transform;
object-fit: contain;
visibility: ${({ $hidden }) => ($hidden ? 'hidden' : 'visible')};
transform: ${({ hovered }) => hovered && 'scale(1.15)'};
background: ${({ theme, imageLoading }) =>
imageLoading && `linear-gradient(270deg, ${theme.backgroundOutline} 0%, ${theme.backgroundSurface} 100%)`};
`
const Image = ({
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
@@ -300,40 +350,50 @@ const Image = ({
}
return (
<Box display="flex" overflow="hidden" borderTopLeftRadius={BORDER_RADIUS} borderTopRightRadius={BORDER_RADIUS}>
<Box
as="img"
width="full"
style={{
aspectRatio: `${uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'}`,
transition: 'transform 0.25s ease 0s',
}}
<StyledMediaContainer>
<StyledImage
src={asset.imageUrl || asset.smallImageUrl}
objectFit="contain"
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
hovered={hovered && !isMobile}
imageLoading={!loaded}
draggable={false}
onError={() => setNoContent(true)}
onLoad={(e) => {
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
setLoaded(true)
}}
className={clsx(hovered && !isMobile && styles.cardImageHover, !loaded && styles.loadingBackground)}
/>
</Box>
</StyledMediaContainer>
)
}
function getMediaAspectRatio(
uniformAspectRatio: UniformAspectRatio,
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
): string {
return uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
}
interface MediaProps {
shouldPlay: boolean
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
}
const PlaybackButton = styled.div`
position: absolute;
height: 40px;
width: 40px;
z-index: 1;
margin-left: calc(100% - 50px);
transform: translateY(-56px);
`
const StyledVideo = styled.video<{
$aspectRatio?: string
}>`
width: 100%;
aspect-ratio: ${({ $aspectRatio }) => $aspectRatio};
`
const StyledInnerMediaContainer = styled(Row)`
position: absolute;
left: 0px;
top: 0px;
`
const Video = ({
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
@@ -360,52 +420,38 @@ const Video = ({
return (
<>
<Box display="flex" overflow="hidden">
<Box
as="img"
alt={asset.name || asset.tokenId}
width="full"
style={{
aspectRatio: getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio),
transition: 'transform 0.25s ease 0s',
willChange: 'transform',
}}
<StyledMediaContainer>
<StyledImage
src={asset.imageUrl || asset.smallImageUrl}
objectFit="contain"
alt={asset.name || asset.tokenId}
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
hovered={hovered && !isMobile}
imageLoading={!imageLoaded}
draggable={false}
onError={() => setNoContent(true)}
onLoad={(e) => {
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
setImageLoaded(true)
}}
visibility={shouldPlay ? 'hidden' : 'visible'}
className={clsx(hovered && !isMobile && styles.cardImageHover, !imageLoaded && styles.loadingBackground)}
$hidden={shouldPlay}
/>
</Box>
</StyledMediaContainer>
{shouldPlay ? (
<>
<Box className={styles.playbackSwitch}>
<PauseButtonIcon
width="100%"
height="100%"
<PlaybackButton>
<Pause
size="24px"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setCurrentTokenPlayingMedia(undefined)
}}
className="playback-icon"
/>
</Box>
<Box position="absolute" left="0" top="0" display="flex">
<Box
as="video"
</PlaybackButton>
<StyledInnerMediaContainer>
<StyledVideo
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
ref={vidRef}
width="full"
style={{
aspectRatio: `${
uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
}`,
}}
onEnded={(e) => {
e.preventDefault()
setCurrentTokenPlayingMedia(undefined)
@@ -414,29 +460,32 @@ const Video = ({
playsInline
>
<source src={asset.animationUrl} />
</Box>
</Box>
</StyledVideo>
</StyledInnerMediaContainer>
</>
) : (
<Box className={styles.playbackSwitch}>
<PlaybackButton>
{((!isMobile && hovered) || isMobile) && (
<PlayButtonIcon
width="100%"
height="100%"
<Play
size="24px"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setCurrentTokenPlayingMedia(asset.tokenId)
}}
className="playback-icon"
/>
)}
</Box>
</PlaybackButton>
)}
</>
)
}
const StyledAudio = styled.audio`
width: 100%;
height: 100%;
`
const Audio = ({
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
@@ -463,29 +512,25 @@ const Audio = ({
return (
<>
<Box display="flex" overflow="hidden">
<Box
as="img"
alt={asset.name || asset.tokenId}
width="full"
style={{
aspectRatio: getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio),
transition: 'transform 0.4s ease 0s',
}}
<StyledMediaContainer>
<StyledImage
src={asset.imageUrl || asset.smallImageUrl}
objectFit="contain"
alt={asset.name || asset.tokenId}
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
hovered={hovered && !isMobile}
imageLoading={!imageLoaded}
draggable={false}
onError={() => setNoContent(true)}
onLoad={(e) => {
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
setImageLoaded(true)
setImageLoaded(true)
}}
className={clsx(hovered && !isMobile && styles.cardImageHover, !imageLoaded && styles.loadingBackground)}
/>
</Box>
</StyledMediaContainer>
{shouldPlay ? (
<>
<Box className={styles.playbackSwitch}>
<PlaybackButton>
<PauseButtonIcon
width="100%"
height="100%"
@@ -494,26 +539,22 @@ const Audio = ({
e.stopPropagation()
setCurrentTokenPlayingMedia(undefined)
}}
className="playback-icon"
/>
</Box>
<Box position="absolute" left="0" top="0" display="flex">
<Box
as="audio"
</PlaybackButton>
<StyledInnerMediaContainer>
<StyledAudio
ref={audRef}
width="full"
height="full"
onEnded={(e) => {
e.preventDefault()
setCurrentTokenPlayingMedia(undefined)
}}
>
<source src={asset.animationUrl} />
</Box>
</Box>
</StyledAudio>
</StyledInnerMediaContainer>
</>
) : (
<Box className={styles.playbackSwitch}>
<PlaybackButton>
{((!isMobile && hovered) || isMobile) && (
<PlayButtonIcon
width="100%"
@@ -523,10 +564,9 @@ const Audio = ({
e.stopPropagation()
setCurrentTokenPlayingMedia(asset.tokenId)
}}
className="playback-icon"
/>
)}
</Box>
</PlaybackButton>
)}
</>
)
@@ -537,36 +577,39 @@ interface CardDetailsContainerProps {
children: ReactNode
}
const StyledDetailsContainer = styled(Column)`
position: relative;
padding: 12px 12px 0px;
justify-content: space-between;
transition: ${({ theme }) => `${theme.transition.duration.medium}`};
`
const DetailsContainer = ({ children }: CardDetailsContainerProps) => {
return (
<Row
position="relative"
paddingX="12"
paddingTop="12"
justifyContent="space-between"
flexDirection="column"
transition="250"
>
{children}
</Row>
)
return <StyledDetailsContainer>{children}</StyledDetailsContainer>
}
const StyledInfoContainer = styled.div`
overflow: hidden;
width: 100%;
`
const InfoContainer = ({ children }: { children: ReactNode }) => {
return (
<Box overflow="hidden" width="full">
{children}
</Box>
)
return <StyledInfoContainer>{children}</StyledInfoContainer>
}
const TruncatedTextRow = styled(Row)`
const TruncatedTextRow = styled(ThemedText.BodySmall)`
display: flex;
padding: 2px;
white-space: pre;
text-overflow: ellipsis;
display: block;
overflow: hidden;
flex: 1;
`
const AssetNameRow = styled(TruncatedTextRow)`
color: ${({ theme }) => theme.textPrimary};
font-size: 16px !important;
font-weight: 400;
`
interface ProfileNftDetailsProps {
@@ -574,6 +617,18 @@ interface ProfileNftDetailsProps {
hideDetails: boolean
}
const PrimaryRowContainer = styled.div`
overflow: hidden;
width: 100%;
flex-wrap: nowrap;
`
const FloorPriceRow = styled(TruncatedTextRow)`
font-size: 16px;
font-weight: 600;
line-height: 20px;
`
const ProfileNftDetails = ({ asset, hideDetails }: ProfileNftDetailsProps) => {
const assetName = () => {
if (!asset.name && !asset.tokenId) return
@@ -583,89 +638,95 @@ const ProfileNftDetails = ({ asset, hideDetails }: ProfileNftDetailsProps) => {
const shouldShowUserListedPrice = !asset.notForSale && asset.asset_contract.tokenType !== NftStandard.Erc1155
return (
<Box overflow="hidden" width="full" flexWrap="nowrap">
<PrimaryRowContainer>
<PrimaryRow>
<PrimaryDetails>
<TruncatedTextRow className={bodySmall} style={{ color: themeVars.colors.textSecondary }}>
<TruncatedTextRow color="textSecondary">
{!!asset.asset_contract.name && <span>{asset.asset_contract.name}</span>}
</TruncatedTextRow>
{asset.collectionIsVerified && <VerifiedIcon height="18px" width="18px" />}
</PrimaryDetails>
{!hideDetails && <DetailsLink />}
</PrimaryRow>
<Row justifyItems="flex-start">
<TruncatedTextRow
className={body}
style={{
color: themeVars.colors.textPrimary,
}}
>
{assetName()}
</TruncatedTextRow>
<Row>
<AssetNameRow>{assetName()}</AssetNameRow>
{asset.susFlag && <Suspicious />}
</Row>
<TruncatedTextRow className={buttonTextMedium} style={{ color: themeVars.colors.textPrimary }}>
<FloorPriceRow>
{shouldShowUserListedPrice && asset.floor_sell_order_price
? `${floorFormatter(asset.floor_sell_order_price)} ETH`
: ' '}
</TruncatedTextRow>
</Box>
</FloorPriceRow>
</PrimaryRowContainer>
)
}
const PrimaryRow = ({ children }: { children: ReactNode }) => (
<Row gap="8" justifyContent="space-between">
{children}
</Row>
)
const StyledPrimaryRow = styled(Row)`
gap: 8px;
justify-content: space-between;
`
const PrimaryRow = ({ children }: { children: ReactNode }) => <StyledPrimaryRow>{children}</StyledPrimaryRow>
const StyledPrimaryDetails = styled(Row)`
justify-items: center;
overflow: hidden;
white-space: nowrap;
`
const PrimaryDetails = ({ children }: { children: ReactNode }) => (
<Row justifyItems="center" overflow="hidden" whiteSpace="nowrap">
{children}
</Row>
<StyledPrimaryDetails>{children}</StyledPrimaryDetails>
)
const PrimaryInfoContainer = styled.div`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 400;
font-size: 16px;
line-height: 24px;
`
const PrimaryInfo = ({ children }: { children: ReactNode }) => {
return (
<Box overflow="hidden" whiteSpace="nowrap" textOverflow="ellipsis" className={body}>
{children}
</Box>
)
return <PrimaryInfoContainer>{children}</PrimaryInfoContainer>
}
const SecondaryRow = ({ children }: { children: ReactNode }) => (
<Row height="20" justifyContent="space-between" marginTop="6">
{children}
</Row>
)
const StyledSecondaryRow = styled(Row)`
height: 20px;
justify-content: space-between;
margin-top: 6px;
`
const SecondaryRow = ({ children }: { children: ReactNode }) => <StyledSecondaryRow>{children}</StyledSecondaryRow>
const StyledSecondaryDetails = styled(Row)`
overflow: hidden;
white-space: nowrap;
`
const SecondaryDetails = ({ children }: { children: ReactNode }) => (
<Row overflow="hidden" whiteSpace="nowrap">
{children}
</Row>
<StyledSecondaryDetails>{children}</StyledSecondaryDetails>
)
const SecondaryInfoContainer = styled.div`
color: ${({ theme }) => theme.textPrimary};
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 20px;
`
const SecondaryInfo = ({ children }: { children: ReactNode }) => {
return (
<Box
color="textPrimary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
style={{ lineHeight: '20px' }}
className={subhead}
>
{children}
</Box>
)
return <SecondaryInfoContainer>{children}</SecondaryInfoContainer>
}
const TertiaryInfoContainer = styled.div`
color: ${({ theme }) => theme.textSecondary};
margin-top: 8px;
`
const TertiaryInfo = ({ children }: { children: ReactNode }) => {
return (
<Box marginTop="8" color="textSecondary">
{children}
</Box>
)
return <TertiaryInfoContainer>{children}</TertiaryInfoContainer>
}
interface Erc1155ControlsInterface {
@@ -700,15 +761,18 @@ const Erc1155Controls = ({ quantity }: Erc1155ControlsInterface) => {
)
}
const StyledMarketplaceIcon = styled.img`
display: inline-block;
width: 16px;
height: 16px;
border-radius: 4px;
flex-shrink: 0;
margin-left: 8px;
vertical-align: top;
`
const MarketplaceIcon = ({ marketplace }: { marketplace: string }) => {
return (
<Box
as="img"
alt={marketplace}
src={`/nft/svgs/marketplaces/${marketplace}.svg`}
className={styles.marketplaceIcon}
/>
)
return <StyledMarketplaceIcon alt={marketplace} src={`/nft/svgs/marketplaces/${marketplace}.svg`} />
}
const DetailsLink = () => {
@@ -721,7 +785,7 @@ const DetailsLink = () => {
e.stopPropagation()
}}
>
<Box data-testid="nft-details-link">Details</Box>
<div data-testid="nft-details-link">Details</div>
</DetailsLinkContainer>
)
}
@@ -734,6 +798,28 @@ interface RankingProps {
rarityLogo?: string
}
const RarityLogoContainer = styled(Row)`
margin-right: 4px;
width: 16px;
`
const RarityText = styled(ThemedText.BodySmall)`
display: flex;
`
const RarityInfo = styled(Row)`
height: 16px;
border-radius: 4px;
color: ${({ theme }) => theme.textPrimary};
background: ${({ theme }) => theme.backgroundInteractive};
font-size: 10px;
font-weight: 600;
padding: 0px 4px;
line-height: 12px;
letter-spacing: 0.04em;
backdrop-filter: blur(6px);
`
const Ranking = ({ rarity, provider, rarityVerified, rarityLogo }: RankingProps) => {
const { asset } = useCardContext()
@@ -744,44 +830,49 @@ const Ranking = ({ rarity, provider, rarityVerified, rarityLogo }: RankingProps)
<MouseoverTooltip
text={
<Row>
<Box display="flex" marginRight="4">
<img src={rarityLogo} alt="cardLogo" width={16} />
</Box>
<Box width="full" className={bodySmall}>
<RarityLogoContainer>
<img src={rarityLogo} alt="cardLogo" width={16} height={16} />
</RarityLogoContainer>
<RarityText>
{rarityVerified
? `Verified by ${
('collectionName' in asset && asset.collectionName) ||
('asset_contract' in asset && asset.asset_contract?.name)
}`
: `Ranking by ${rarity.primaryProvider === 'Genie' ? fallbackProvider : rarity.primaryProvider}`}
</Box>
</RarityText>
</Row>
}
placement="top"
>
<Box className={styles.rarityInfo}>
<Box paddingTop="2" paddingBottom="2" display="flex">
{putCommas(provider.rank)}
</Box>
<Box display="flex" height="16">
{rarityVerified ? <RarityVerifiedIcon /> : null}
</Box>
</Box>
<RarityInfo>
<Row padding="2px 0px">{putCommas(provider.rank)}</Row>
<Row>{rarityVerified ? <RarityVerifiedIcon /> : null}</Row>
</RarityInfo>
</MouseoverTooltip>
</RankingContainer>
)}
</>
)
}
const SUSPICIOUS_TEXT = 'Blocked on OpenSea'
const SUSPICIOUS_TEXT = t`Blocked on OpenSea`
const SuspiciousIconContainer = styled(Row)`
flex-shrink: 0;
margin-left: 4px;
`
const PoolIconContainer = styled(SuspiciousIconContainer)`
color: ${({ theme }) => theme.textSecondary};
`
const Suspicious = () => {
return (
<MouseoverTooltip text={<Box className={bodySmall}>{SUSPICIOUS_TEXT}</Box>} placement="top">
<Box display="flex" flexShrink="0" marginLeft="4">
<MouseoverTooltip text={<ThemedText.BodySmall>{SUSPICIOUS_TEXT}</ThemedText.BodySmall>} placement="top">
<SuspiciousIconContainer>
<SuspiciousIcon />
</Box>
</SuspiciousIconContainer>
</MouseoverTooltip>
)
}
@@ -790,44 +881,46 @@ const Pool = () => {
return (
<MouseoverTooltip
text={
<Box className={bodySmall}>
<ThemedText.BodySmall>
This NFT is part of a liquidity pool. Buying this will increase the price of the remaining pooled NFTs.
</Box>
</ThemedText.BodySmall>
}
placement="top"
>
<Box display="flex" flexShrink="0" marginLeft="4" color="textSecondary">
<PoolIconContainer>
<PoolIcon width="20" height="20" />
</Box>
</PoolIconContainer>
</MouseoverTooltip>
)
}
const NoContentContainerBackground = styled.div<{ height?: number }>`
position: relative;
width: 100%;
height: ${({ height }) => (height ? `${height}px` : 'auto')};
padding-top: 100%;
background: ${({ theme }) =>
`linear-gradient(90deg, ${theme.backgroundSurface} 0%, ${theme.backgroundInteractive} 95.83%)`};
`
const NoContentText = styled(ThemedText.BodyPrimary)`
position: absolute;
text-align: center;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
color: ${colors.gray500};
`
const NoContentContainer = ({ height }: { height?: number }) => (
<>
<Box
position="relative"
width="full"
style={{
height: height ? `${height}px` : 'auto',
paddingTop: '100%',
background: `linear-gradient(90deg, ${themeVars.colors.backgroundSurface} 0%, ${themeVars.colors.backgroundInteractive} 95.83%)`,
}}
>
<Box
position="absolute"
textAlign="center"
left="1/2"
top="1/2"
style={{ transform: 'translate3d(-50%, -50%, 0)' }}
color="gray500"
className={body}
>
Content not
<NoContentContainerBackground height={height}>
<NoContentText>
<Trans>Content not</Trans>
<br />
available yet
</Box>
</Box>
<Trans>available yet</Trans>
</NoContentText>
</NoContentContainerBackground>
</>
)

View File

@@ -240,21 +240,36 @@ export const RarityVerifiedIcon = () => (
</svg>
)
export const EditPriceIcon = (props: SVGProps) => (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.4713 5.06496L13.2161 4.28041C13.5935 3.88317 13.6233 3.33696 13.2459 2.95958L12.9877 2.69144C12.65 2.35378 12.084 2.40344 11.7165 2.77089L10.9419 3.52565L12.4713 5.06496ZM3.10986 13.2347L5.14573 12.3806L11.7463 5.78L10.2169 4.26055L3.61635 10.8711L2.72255 12.8374C2.62324 13.0658 2.88145 13.324 3.10986 13.2347Z"
fill="#70757A"
/>
</svg>
)
export const AttachPriceIcon = (props: SVGProps) => (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.76353 9.88671L8.53195 9.10825C7.93931 9.05803 7.51242 8.86216 7.20605 8.5558C6.35728 7.70702 6.35728 6.50669 7.20103 5.66796L8.86844 3.99553C9.71722 3.15178 10.9125 3.14676 11.7613 3.99553C12.6151 4.84932 12.6051 6.04464 11.7663 6.88839L10.9125 7.73716C11.0732 8.10881 11.1285 8.56082 11.0381 8.95256L12.4745 7.5212C13.71 6.29073 13.715 4.53292 12.4694 3.28738C11.2189 2.03682 9.47112 2.04687 8.23563 3.28236L6.48786 5.03013C5.25237 6.26562 5.24735 8.01841 6.49289 9.26394C6.78418 9.56026 7.18597 9.78124 7.76353 9.88671ZM8.23061 5.64285L7.46219 6.42131C8.05483 6.47655 8.48172 6.6674 8.78809 6.97376C9.64188 7.82254 9.63686 9.02287 8.79311 9.86662L7.1257 11.534C6.27693 12.3828 5.08161 12.3828 4.23786 11.534C3.38407 10.6802 3.38909 9.48995 4.23284 8.6462L5.08161 7.7924C4.9209 7.42577 4.87068 6.97376 4.95605 6.577L3.51967 8.01339C2.28418 9.24385 2.27916 10.9966 3.52469 12.2422C4.77525 13.4927 6.52302 13.4827 7.75851 12.2522L9.50628 10.4994C10.7418 9.26394 10.7468 7.51115 9.50126 6.26562C9.20996 5.97432 8.8132 5.75334 8.23061 5.64285Z"
fill="#4673FA"
/>
export const BrokenLinkIcon = (props: SVGProps) => (
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<g clipPath="url(#clip0_79_4612)">
<path
d="M14.4344 11.3181L16.9344 8.81813C17.6934 8.03229 18.1133 6.97978 18.1039 5.8873C18.0944 4.79481 17.6562 3.74976 16.8836 2.97722C16.1111 2.20469 15.066 1.76649 13.9735 1.75699C12.8811 1.7475 11.8286 2.16748 11.0427 2.92647L9.60938 4.35147"
stroke="#98A1C0"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.20088 8.75098L2.70088 11.251C1.94189 12.0368 1.52191 13.0893 1.53141 14.1818C1.5409 15.2743 1.9791 16.3194 2.75164 17.0919C3.52417 17.8644 4.56922 18.3026 5.66171 18.3121C6.7542 18.3216 7.80671 17.9016 8.59255 17.1426L10.0175 15.7176"
stroke="#98A1C0"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5 3.24316L14.7368 16.6952"
stroke="#98A1C0"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_79_4612">
<rect width="20" height="20" fill="white" transform="translate(0.128906 0.0341797)" />
</clipPath>
</defs>
</svg>
)
@@ -289,30 +304,6 @@ export const ClockIcon = () => (
</svg>
)
export const LoadingIcon = (props: SVGProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="100px"
height="100px"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
{...props}
>
<circle cx="50" cy="50" fill="none" strokeWidth="10" r="35" strokeDasharray="164.93361431346415 56.97787143782138">
<animateTransform
attributeName="transform"
type="rotate"
repeatCount="indefinite"
dur="1s"
values="0 50 50;360 50 50"
keyTimes="0;1"
{...props}
></animateTransform>
</circle>
</svg>
)
export const ApprovedCheckmarkIcon = (props: SVGProps) => (
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
@@ -322,25 +313,6 @@ export const ApprovedCheckmarkIcon = (props: SVGProps) => (
</svg>
)
export const HazardIcon = () => (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8.57227" y="6.66669" width="2.85714" height="8.57143" fill="white" />
<path
d="M5.14158 15.7143H14.8489C15.8017 15.7143 16.4294 15.0318 16.4294 14.1977C16.4294 13.9377 16.3509 13.6885 16.2052 13.4502L11.3516 5.05483C11.0489 4.53486 10.5221 4.28571 9.99523 4.28571C9.46839 4.28571 8.93034 4.53486 8.6389 5.05483L3.78524 13.4502C3.63952 13.6885 3.57227 13.9377 3.57227 14.1977C3.57227 15.0318 4.18878 15.7143 5.14158 15.7143ZM9.99523 11.4245C9.56928 11.4245 9.25542 11.1428 9.24421 10.7312L9.15453 7.71969C9.14332 7.24305 9.49081 6.90724 9.99523 6.90724C10.5109 6.90724 10.8584 7.23222 10.8471 7.71969L10.7575 10.7312C10.7351 11.1428 10.4324 11.4245 9.99523 11.4245ZM9.99523 14.0677C9.43477 14.0677 8.98639 13.6452 8.98639 13.1036C8.98639 12.5728 9.43477 12.1503 9.99523 12.1503C10.5669 12.1503 11.0153 12.5836 11.0153 13.1036C11.0041 13.6452 10.5669 14.0677 9.99523 14.0677Z"
fill="#F95E14"
/>
</svg>
)
export const FailedListingIcon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M9.9933 16.2444C13.5529 16.2444 16.4909 13.3064 16.4909 9.75307C16.4909 6.19978 13.5466 3.26172 9.98703 3.26172C6.43373 3.26172 3.50195 6.19978 3.50195 9.75307C3.50195 13.3064 6.44001 16.2444 9.9933 16.2444ZM8.12877 12.3207C7.78976 12.3207 7.62653 12.1324 7.62653 11.8624V7.63742C7.62653 7.36747 7.78976 7.17913 8.12877 7.17913H8.80678C9.14579 7.17913 9.30901 7.36747 9.30901 7.63742V11.8624C9.30901 12.1324 9.14579 12.3207 8.80678 12.3207H8.12877ZM11.1798 12.3207C10.8471 12.3207 10.6776 12.1324 10.6776 11.8624V7.63742C10.6776 7.36747 10.8471 7.17913 11.1798 7.17913H11.8641C12.1906 7.17913 12.3538 7.36747 12.3538 7.63742V11.8624C12.3538 12.1324 12.1906 12.3207 11.8641 12.3207H11.1798Z"
fill={themeVars.colors.textSecondary}
/>
</svg>
)
export const FilterIcon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="1 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path

View File

@@ -16,6 +16,6 @@ export const overlay = style([
{
opacity: 0.72,
overflow: 'hidden',
zIndex: Z_INDEX.modalBackdrop + 1,
zIndex: Z_INDEX.modalBackdrop - 2,
},
])

View File

@@ -1,22 +1,29 @@
import { t, Trans } from '@lingui/macro'
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
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 { approveCollectionRow, getListingState, getTotalEthValue, verifyStatus } from 'nft/components/bag/profile/utils'
import { useBag, useIsMobile, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ListingButton } from 'nft/components/profile/list/ListingButton'
import {
approveCollectionRow,
getTotalEthValue,
useSubscribeListingState,
verifyStatus,
} from 'nft/components/profile/list/utils'
import { useIsMobile, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
import { LIST_PAGE_MARGIN, LIST_PAGE_MARGIN_MOBILE } from 'nft/pages/profile/shared'
import { looksRareNonceFetcher } from 'nft/queries'
import { ListingStatus, ProfilePageStateType } from 'nft/types'
import { fetchPrice, formatEth, formatUsdPrice } from 'nft/utils'
import { ProfilePageStateType } from 'nft/types'
import { formatEth } from 'nft/utils'
import { ListingMarkets } from 'nft/utils/listNfts'
import { useEffect, useMemo, useReducer, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import styled, { css } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import shallow from 'zustand/shallow'
@@ -81,20 +88,11 @@ const ButtonsWrapper = styled(Row)`
width: min-content;
`
const MarketWrap = styled.section<{ isNftListV2: boolean }>`
const MarketWrap = styled.section`
gap: 48px;
margin: 0px auto;
width: 100%;
max-width: 1200px;
${({ isNftListV2 }) => !isNftListV2 && v1Padding}
`
const v1Padding = css`
padding: 0px 16px;
@media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) {
padding: 0px 44px;
}
`
const ListingHeaderRow = styled(Row)`
@@ -112,15 +110,6 @@ const GridWrapper = styled.div`
margin-bottom: 48px;
`
const MobileListButtonWrapper = styled.div`
display: flex;
margin: 14px 16px 32px 16px;
@media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) {
display: none;
}
`
const FloatingConfirmationBar = styled(Row)<{ issues: boolean }>`
padding: 12px 12px 12px 32px;
border: 1px solid;
@@ -195,9 +184,7 @@ const EthValueWrapper = styled.span<{ totalEthListingValue: boolean }>`
export const ListPage = () => {
const { setProfilePageState: setSellPageState } = useProfilePageState()
const { provider } = useWeb3React()
const toggleBag = useBag((s) => s.toggleBag)
const isMobile = useIsMobile()
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
const { setGlobalMarketplaces, sellAssets, issues } = useSellAsset(
({ setGlobalMarketplaces, sellAssets, issues }) => ({
@@ -207,26 +194,10 @@ export const ListPage = () => {
}),
shallow
)
const {
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionStatusAndCallback,
} = useNFTList(
({
const { listings, collectionsRequiringApproval, setLooksRareNonce, setCollectionStatusAndCallback } = useNFTList(
({ listings, collectionsRequiringApproval, setLooksRareNonce, setCollectionStatusAndCallback }) => ({
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionStatusAndCallback,
}) => ({
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionStatusAndCallback,
}),
@@ -234,32 +205,16 @@ export const ListPage = () => {
)
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const anyListingsMissingPrice = useMemo(() => !!listings.find((listing) => !listing.price), [listings])
const nativeCurrency = useNativeCurrency()
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
const usdcValue = useStablecoinValue(parsedAmount)
const usdcAmount = formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)
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) => {
setEthPriceInUSD(price ?? 0)
})
}, [])
// TODO with removal of list v1 see if this logic can be removed
useEffect(() => {
const state = getListingState(collectionsRequiringApproval, listings)
if (state.allListingsApproved) setListingStatus(ListingStatus.APPROVED)
else if (state.anyPaused && !state.anyActiveFailures && !state.anyActiveSigning && !state.anyActiveRejections) {
setListingStatus(ListingStatus.CONTINUE)
} else if (state.anyPaused) setListingStatus(ListingStatus.PAUSED)
else if (state.anyActiveSigning) setListingStatus(ListingStatus.SIGNING)
else if (state.allListingsPending || (state.allCollectionsPending && state.allListingsDefined))
setListingStatus(ListingStatus.PENDING)
else if (state.anyActiveFailures && listingStatus !== ListingStatus.PAUSED) setListingStatus(ListingStatus.FAILED)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listings, collectionsRequiringApproval])
// instantiate listings and collections to approve when users modify input data
useSubscribeListingState()
useEffect(() => {
setGlobalMarketplaces(selectedMarkets)
@@ -270,14 +225,13 @@ export const ListPage = () => {
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,
usd_value: usdcAmount,
...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)
@@ -291,7 +245,7 @@ export const ListPage = () => {
}
}
const handleV2Click = () => {
const showModalAndStartListing = () => {
toggleShowListModal()
startListingFlow()
}
@@ -308,7 +262,7 @@ export const ListPage = () => {
return (
<Column>
<MarketWrap isNftListV2={isNftListV2}>
<MarketWrap>
<ListingHeader>
<Row>
<ArrowContainer>
@@ -332,39 +286,22 @@ export const ListPage = () => {
<NFTListingsGrid selectedMarkets={selectedMarkets} />
</GridWrapper>
</MarketWrap>
{isNftListV2 && (
<>
<FloatingConfirmationBar issues={!!issues}>
{BannerText}
<ProceedsAndButtonWrapper>
<ProceedsWrapper>
<EthValueWrapper totalEthListingValue={!!totalEthListingValue}>
{totalEthListingValue > 0 ? formatEth(totalEthListingValue) : '-'} ETH
</EthValueWrapper>
{!!totalEthListingValue && !!ethPriceInUSD && (
<UsdValue>{formatUsdPrice(totalEthListingValue * ethPriceInUSD)}</UsdValue>
)}
</ProceedsWrapper>
<ListingButton
onClick={handleV2Click}
buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`}
showWarningOverride={true}
/>
</ProceedsAndButtonWrapper>
</FloatingConfirmationBar>
<Overlay />
</>
)}
{!isNftListV2 && (
<MobileListButtonWrapper>
<ListingButton onClick={toggleBag} buttonText="Continue listing" />
</MobileListButtonWrapper>
)}
{isNftListV2 && showListModal && (
<>
<ListModal overlayClick={toggleShowListModal} />
</>
)}
<FloatingConfirmationBar issues={!!issues}>
{BannerText}
<ProceedsAndButtonWrapper>
<ProceedsWrapper>
<EthValueWrapper totalEthListingValue={!!totalEthListingValue}>
{totalEthListingValue > 0 ? formatEth(totalEthListingValue) : '-'} ETH
</EthValueWrapper>
{!!usdcValue && <UsdValue>{usdcAmount}</UsdValue>}
</ProceedsWrapper>
<ListingButton onClick={showModalAndStartListing} />
</ProceedsAndButtonWrapper>
</FloatingConfirmationBar>
<Overlay />
{showListModal && <ListModal overlayClick={toggleShowListModal} />}
</Column>
)
}

View File

@@ -0,0 +1,126 @@
import { Plural, t, Trans } from '@lingui/macro'
import { BaseButton } from 'components/Button'
import ms from 'ms.macro'
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
import { useIsMobile, useSellAsset } from 'nft/hooks'
import { Listing, WalletAsset } from 'nft/types'
import { useMemo, useState } from 'react'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import shallow from 'zustand/shallow'
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
const StyledListingButton = styled(BaseButton)<{ showResolveIssues: boolean; missingPrices: boolean }>`
background: ${({ showResolveIssues, theme }) => (showResolveIssues ? theme.accentFailure : theme.accentAction)};
color: ${({ theme }) => theme.accentTextLightPrimary};
font-weight: 600;
font-size: 20px;
line-height: 24px;
padding: 16px;
border-radius: 12px;
width: min-content;
border: none;
cursor: ${({ missingPrices }) => (missingPrices ? 'auto' : 'pointer')};
opacity: ${({ showResolveIssues, missingPrices }) => !showResolveIssues && missingPrices && '0.3'};
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
font-size: 16px;
line-height: 20px;
padding: 10px 12px;
}
`
export const ListingButton = ({ onClick }: { onClick: () => void }) => {
const { sellAssets, showResolveIssues, toggleShowResolveIssues, issues, setIssues } = useSellAsset(
({ sellAssets, showResolveIssues, toggleShowResolveIssues, issues, setIssues }) => ({
sellAssets,
showResolveIssues,
toggleShowResolveIssues,
issues,
setIssues,
}),
shallow
)
const [showWarning, setShowWarning] = useState(false)
const isMobile = useIsMobile()
// Find issues with item listing data
const [listingsMissingPrice, listingsBelowFloor] = useMemo(() => {
const missingExpiration = sellAssets.some((asset) => {
return (
asset.expirationTime != null &&
(isNaN(asset.expirationTime) || asset.expirationTime * 1000 - Date.now() < ms`60 seconds`)
)
})
const overMaxExpiration = sellAssets.some((asset) => {
return asset.expirationTime != null && asset.expirationTime * 1000 - Date.now() > ms`180 days`
})
const listingsMissingPrice: [WalletAsset, Listing][] = []
const listingsBelowFloor: [WalletAsset, Listing][] = []
const listingsAboveSellOrderFloor: [WalletAsset, Listing][] = []
const invalidPrices: [WalletAsset, Listing][] = []
for (const asset of sellAssets) {
if (asset.newListings) {
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) * BELOW_FLOOR_PRICE_THRESHOLD &&
!listing.overrideFloorPrice
)
listingsBelowFloor.push([asset, listing])
else if (asset.floor_sell_order_price && listing.price >= asset.floor_sell_order_price)
listingsAboveSellOrderFloor.push([asset, listing])
}
}
}
// set number of issues
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()
return [listingsMissingPrice, listingsBelowFloor]
}, [sellAssets, setIssues, showResolveIssues, toggleShowResolveIssues])
const warningWrappedClick = () => {
if (issues) !showResolveIssues && toggleShowResolveIssues()
else if (listingsBelowFloor.length) setShowWarning(true)
else onClick()
}
return (
<>
<StyledListingButton
onClick={warningWrappedClick}
missingPrices={!!listingsMissingPrice.length}
showResolveIssues={showResolveIssues}
>
{showResolveIssues ? (
<Plural value={issues !== 1 ? 2 : 1} _1="Resolve issue" other={t`Resolve ${issues} issues`} />
) : listingsMissingPrice.length && !isMobile ? (
<Trans>Set prices to continue</Trans>
) : (
<Trans>Start listing</Trans>
)}
</StyledListingButton>
{showWarning && (
<BelowFloorWarningModal
listingsBelowFloor={listingsBelowFloor}
closeModal={() => setShowWarning(false)}
startListing={onClick}
/>
)}
</>
)
}

View File

@@ -4,19 +4,18 @@ import Column from 'components/Column'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { RowsCollpsedIcon, RowsExpandedIcon } from 'nft/components/icons'
import { getRoyalty, useHandleGlobalPriceToggle, useSyncPriceWithGlobalMethod } from 'nft/components/profile/list/utils'
import { useSellAsset } from 'nft/hooks'
import { ListingMarket, ListingWarning, WalletAsset } from 'nft/types'
import { LOOKS_RARE_CREATOR_BASIS_POINTS } from 'nft/utils'
import { ListingMarket, WalletAsset } from 'nft/types'
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { Dispatch, DispatchWithoutAction, useEffect, useMemo, useReducer, useState } from 'react'
import { Dispatch, DispatchWithoutAction, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { SetPriceMethod } from './NFTListingsGrid'
import { PriceTextInput } from './PriceTextInput'
import { RoyaltyTooltip } from './RoyaltyTooltip'
import { RemoveIconWrap } from './shared'
import { RemoveIconWrap, SetPriceMethod } from './shared'
const LastPriceInfo = styled(Column)`
text-align: left;
@@ -104,13 +103,6 @@ const ReturnColumn = styled(Column)`
}
`
const getRoyalty = (listingMarket: ListingMarket, asset: WalletAsset) => {
// LooksRare is a unique case where royalties for creators are a flat 0.5% or 50 basis points
const baseFee = listingMarket.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset.basisPoints ?? 0
return baseFee * 0.01
}
interface MarketplaceRowProps {
globalPriceMethod?: SetPriceMethod
globalPrice?: number
@@ -118,7 +110,6 @@ interface MarketplaceRowProps {
selectedMarkets: ListingMarket[]
removeMarket?: () => void
asset: WalletAsset
showMarketplaceLogo: boolean
expandMarketplaceRows?: boolean
rowHovered?: boolean
toggleExpandMarketplaceRows: DispatchWithoutAction
@@ -131,20 +122,31 @@ export const MarketplaceRow = ({
selectedMarkets,
removeMarket = undefined,
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 [marketIconHovered, toggleMarketIconHovered] = useReducer((s) => !s, false)
const [marketRowHovered, toggleMarketRowHovered] = useReducer((s) => !s, false)
const [listPrice, setListPrice] = useState<number | undefined>(
() =>
asset.newListings?.find((listing) =>
expandMarketplaceRows ? listing.marketplace.name === selectedMarkets?.[0].name : !!listing.price
)?.price
)
const [globalOverride, setGlobalOverride] = useState(false)
const showGlobalPrice = globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride
const price = showGlobalPrice ? globalPrice : listPrice
const setPrice = useCallback(
(price?: number) => {
showGlobalPrice ? setGlobalPrice(price) : setListPrice(price)
for (const marketplace of selectedMarkets) setAssetListPrice(asset, price, marketplace)
},
[asset, selectedMarkets, setAssetListPrice, setGlobalPrice, showGlobalPrice]
)
const fees = useMemo(() => {
if (selectedMarkets.length === 1) {
@@ -163,79 +165,36 @@ export const MarketplaceRow = ({
const feeInEth = price && (price * fees) / 100
const userReceives = price && feeInEth && price - feeInEth
useMemo(() => {
for (const market of selectedMarkets) {
if (market && asset && asset.basisPoints) {
market.royalty = (market.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset.basisPoints) * 0.01
}
}
}, [asset, selectedMarkets])
useHandleGlobalPriceToggle(globalOverride, setListPrice, setPrice, listPrice, globalPrice)
useSyncPriceWithGlobalMethod(
asset,
setListPrice,
setGlobalPrice,
setGlobalOverride,
listPrice,
globalPrice,
globalPriceMethod
)
// When in Same Price Mode and not overriding, update local price when global price changes
useEffect(() => {
if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) {
setListPrice(asset?.floorPrice)
setGlobalPrice(asset.floorPrice)
} else if (globalPriceMethod === SetPriceMethod.LAST_PRICE) {
setListPrice(asset.lastPrice)
setGlobalPrice(asset.lastPrice)
} else if (globalPriceMethod === SetPriceMethod.SAME_PRICE)
listPrice && !globalPrice ? setGlobalPrice(listPrice) : setListPrice(globalPrice)
setGlobalOverride(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalPriceMethod])
useEffect(() => {
if (selectedMarkets.length)
for (const marketplace of selectedMarkets) setAssetListPrice(asset, listPrice, marketplace)
else setAssetListPrice(asset, listPrice)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPrice])
useEffect(() => {
let price: number | undefined = undefined
if (globalOverride) {
if (!listPrice) setListPrice(globalPrice)
price = listPrice ? listPrice : globalPrice
} else {
price = globalPrice
}
if (selectedMarkets.length) for (const marketplace of selectedMarkets) setAssetListPrice(asset, price, marketplace)
else setAssetListPrice(asset, price)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalOverride])
useEffect(() => {
if (globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride) {
if (selectedMarkets.length)
for (const marketplace of selectedMarkets) setAssetListPrice(asset, globalPrice, marketplace)
else setAssetListPrice(asset, globalPrice)
if (showGlobalPrice) {
setPrice(globalPrice)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalPrice])
let warning: ListingWarning | undefined = undefined
if (asset.listingWarnings && asset.listingWarnings?.length > 0) {
if (showMarketplaceLogo) {
for (const listingWarning of asset.listingWarnings) {
if (listingWarning.marketplace.name === selectedMarkets[0].name) warning = listingWarning
}
} else {
warning = asset.listingWarnings[0]
}
}
return (
<Row onMouseEnter={toggleMarketRowHovered} onMouseLeave={toggleMarketRowHovered}>
<FloorPriceInfo>
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">
<ThemedText.BodyPrimary color="textSecondary" lineHeight="24px">
{asset.floorPrice ? `${asset.floorPrice.toFixed(3)} ETH` : '-'}
</ThemedText.BodySmall>
</ThemedText.BodyPrimary>
</FloorPriceInfo>
<LastPriceInfo>
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">
<ThemedText.BodyPrimary color="textSecondary" lineHeight="24px">
{asset.lastPrice ? `${asset.lastPrice.toFixed(3)} ETH` : '-'}
</ThemedText.BodySmall>
</ThemedText.BodyPrimary>
</LastPriceInfo>
<Row flex="2">
@@ -258,27 +217,14 @@ export const MarketplaceRow = ({
))}
</MarketIconsWrapper>
)}
{globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride ? (
<PriceTextInput
listPrice={globalPrice}
setListPrice={setGlobalPrice}
isGlobalPrice={true}
setGlobalOverride={setGlobalOverride}
globalOverride={globalOverride}
warning={warning}
asset={asset}
/>
) : (
<PriceTextInput
listPrice={listPrice}
setListPrice={setListPrice}
isGlobalPrice={false}
setGlobalOverride={setGlobalOverride}
globalOverride={globalOverride}
warning={warning}
asset={asset}
/>
)}
<PriceTextInput
listPrice={price}
setListPrice={setPrice}
isGlobalPrice={showGlobalPrice}
setGlobalOverride={setGlobalOverride}
globalOverride={globalOverride}
asset={asset}
/>
{rowHovered && ((expandMarketplaceRows && marketRowHovered) || selectedMarkets.length > 1) && (
<ExpandMarketIconWrapper onClick={toggleExpandMarketplaceRows}>
{expandMarketplaceRows ? <RowsExpandedIcon /> : <RowsCollpsedIcon />}

View File

@@ -1,14 +1,17 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import { useWeb3React } from '@web3-react/core'
import { getTotalEthValue, signListingRow } from 'nft/components/bag/profile/utils'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { Portal } from 'nft/components/common/Portal'
import { Overlay } from 'nft/components/modals/Overlay'
import { getTotalEthValue, signListingRow } from 'nft/components/profile/list/utils'
import { useNFTList, useSellAsset } from 'nft/hooks'
import { ListingStatus } from 'nft/types'
import { fetchPrice } from 'nft/utils'
import { useEffect, useMemo, useReducer, useState } from 'react'
import { useCallback, useEffect, useMemo, useReducer } from 'react'
import { X } from 'react-feather'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
@@ -46,64 +49,60 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
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 { setListingStatusAndCallback, setLooksRareNonce, getLooksRareNonce, collectionsRequiringApproval, listings } =
useNFTList(
({
setListingStatusAndCallback,
setLooksRareNonce,
getLooksRareNonce,
collectionsRequiringApproval,
listings,
}) => ({
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 nativeCurrency = useNativeCurrency()
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
const usdcValue = useStablecoinValue(parsedAmount)
const usdcAmount = formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)
const allCollectionsApproved = useMemo(
() => collectionsRequiringApproval.every((collection) => collection.status === ListingStatus.APPROVED),
[collectionsRequiringApproval]
)
const allListingsApproved = useMemo(
() => listings.every((listing) => listing.status === ListingStatus.APPROVED),
[listings]
)
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,
usd_value: usdcAmount,
...trace,
})
}
// Once all collections have been approved, go to next section and start signing listings
useEffect(() => {
if (allCollectionsApproved) {
signListings()
@@ -112,24 +111,28 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allCollectionsApproved])
const closeModalOnClick = useCallback(() => {
allListingsApproved ? window.location.reload() : overlayClick()
}, [allListingsApproved, overlayClick])
// In the case that a user removes all listings via retry logic, close modal
useEffect(() => {
!listings.length && overlayClick()
}, [listings, overlayClick])
!listings.length && closeModalOnClick()
}, [listings, closeModalOnClick])
return (
<Portal>
<Trace modal={InterfaceModalName.NFT_LISTING}>
<ListModalWrapper>
{listingStatus === ListingStatus.APPROVED ? (
<SuccessScreen overlayClick={overlayClick} />
{allListingsApproved ? (
<SuccessScreen overlayClick={closeModalOnClick} />
) : (
<>
<TitleRow>
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>List NFTs</Trans>
</ThemedText.HeadlineSmall>
<X size={24} cursor="pointer" onClick={overlayClick} />
<X size={24} cursor="pointer" onClick={closeModalOnClick} />
</TitleRow>
<ListModalSection
sectionType={Section.APPROVE}
@@ -147,7 +150,7 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
)}
</ListModalWrapper>
</Trace>
<Overlay onClick={overlayClick} />
<Overlay onClick={closeModalOnClick} />
</Portal>
)
}

View File

@@ -6,7 +6,7 @@ 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 { getTotalEthValue } from 'nft/components/profile/list/utils'
import { useSellAsset } from 'nft/hooks'
import { formatEth, generateTweetForList, pluralize } from 'nft/utils'
import { useMemo } from 'react'

View File

@@ -10,7 +10,9 @@ import { BREAKPOINTS, ThemedText } from 'theme'
import { opacify } from 'theme/utils'
import { MarketplaceRow } from './MarketplaceRow'
import { SetPriceMethod } from './NFTListingsGrid'
import { SetPriceMethod } from './shared'
const IMAGE_THUMBNAIL_SIZE = 60
const NFTListRowWrapper = styled(Row)`
padding: 24px 0px;
@@ -23,8 +25,8 @@ const NFTListRowWrapper = styled(Row)`
`
const RemoveIconContainer = styled.div`
width: 44px;
height: 44px;
width: ${IMAGE_THUMBNAIL_SIZE}px;
height: ${IMAGE_THUMBNAIL_SIZE}px;
padding-left: 12px;
align-self: flex-start;
align-items: center;
@@ -51,8 +53,8 @@ const NFTInfoWrapper = styled(Row)`
`
const NFTImage = styled.img`
width: 44px;
height: 44px;
width: ${IMAGE_THUMBNAIL_SIZE}px;
height: ${IMAGE_THUMBNAIL_SIZE}px;
border-radius: 8px;
margin-right: 8px;
`
@@ -85,6 +87,7 @@ const MarketPlaceRowWrapper = styled(Column)`
gap: 24px;
flex: 1.5;
margin-right: 12px;
padding: 6px 0px;
@media screen and (min-width: ${BREAKPOINTS.md}px) {
flex: 2;
@@ -120,10 +123,10 @@ export const NFTListRow = ({
const [hovered, toggleHovered] = useReducer((s) => !s, false)
const theme = useTheme()
// Keep localMarkets up to date with changes to globalMarkets
useEffect(() => {
setLocalMarkets(JSON.parse(JSON.stringify(selectedMarkets)))
selectedMarkets.length < 2 && expandMarketplaceRows && toggleExpandMarketplaceRows()
}, [expandMarketplaceRows, selectedMarkets])
}, [selectedMarkets])
return (
<NFTListRowWrapper
@@ -158,17 +161,16 @@ export const NFTListRow = ({
</TokenInfoWrapper>
</NFTInfoWrapper>
<MarketPlaceRowWrapper>
{expandMarketplaceRows ? (
localMarkets.map((market, index) => {
{expandMarketplaceRows && localMarkets.length > 1 ? (
localMarkets.map((market) => {
return (
<MarketplaceRow
globalPriceMethod={globalPriceMethod}
globalPrice={globalPrice}
setGlobalPrice={setGlobalPrice}
selectedMarkets={[market]}
removeMarket={() => localMarkets.splice(index, 1)}
removeMarket={() => setLocalMarkets(localMarkets.filter((oldMarket) => oldMarket.name !== market.name))}
asset={asset}
showMarketplaceLogo={true}
key={asset.name + market.name}
expandMarketplaceRows={expandMarketplaceRows}
rowHovered={hovered}
@@ -183,7 +185,6 @@ export const NFTListRow = ({
setGlobalPrice={setGlobalPrice}
selectedMarkets={localMarkets}
asset={asset}
showMarketplaceLogo={false}
rowHovered={hovered}
toggleExpandMarketplaceRows={toggleExpandMarketplaceRows}
/>

View File

@@ -1,5 +1,4 @@
import { Trans } from '@lingui/macro'
// eslint-disable-next-line no-restricted-imports
import Column from 'components/Column'
import Row from 'components/Row'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
@@ -12,6 +11,7 @@ import { BREAKPOINTS } from 'theme'
import { Dropdown } from './Dropdown'
import { NFTListRow } from './NFTListRow'
import { SetPriceMethod } from './shared'
const TableHeader = styled.div`
display: flex;
@@ -28,7 +28,7 @@ const TableHeader = styled.div`
line-height: 20px;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
margin-left: 48px;
padding-left: 48px;
}
`
@@ -143,13 +143,6 @@ const RowDivider = styled.hr`
border-color: ${({ theme }) => theme.backgroundInteractive};
`
export enum SetPriceMethod {
SAME_PRICE,
FLOOR_PRICE,
LAST_PRICE,
CUSTOM,
}
export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingMarket[] }) => {
const sellAssets = useSellAsset((state) => state.sellAssets)
const [globalPriceMethod, setGlobalPriceMethod] = useState(SetPriceMethod.CUSTOM)

View File

@@ -1,25 +1,28 @@
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import Row from 'components/Row'
import { AttachPriceIcon, EditPriceIcon } from 'nft/components/icons'
import { BrokenLinkIcon } from 'nft/components/icons'
import { NumericInput } from 'nft/components/layout/Input'
import { useUpdateInputAndWarnings } from 'nft/components/profile/list/utils'
import { body } from 'nft/css/common.css'
import { useSellAsset } from 'nft/hooks'
import { ListingWarning, WalletAsset } from 'nft/types'
import { WalletAsset } from 'nft/types'
import { formatEth } from 'nft/utils/currency'
import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import { Dispatch, useRef, useState } from 'react'
import { AlertTriangle, Link } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { colors } from 'theme/colors'
import { WarningType } from './shared'
const PriceTextInputWrapper = styled(Column)`
gap: 12px;
position: relative;
`
const InputWrapper = styled(Row)<{ borderColor: string }>`
height: 44px;
height: 48px;
color: ${({ theme }) => theme.textTertiary};
padding: 12px;
border: 2px solid;
@@ -34,12 +37,17 @@ const CurrencyWrapper = styled.div<{ listPrice: number | undefined }>`
`
const GlobalPriceIcon = styled.div`
display: block;
display: flex;
cursor: pointer;
position: absolute;
top: -6px;
right: -4px;
bottom: 32px;
right: -10px;
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 50%;
height: 28px;
width: 28px;
align-items: center;
justify-content: center;
`
const WarningRow = styled(Row)`
@@ -66,12 +74,6 @@ const WarningAction = styled.div`
color: ${({ theme }) => theme.accentAction};
`
enum WarningType {
BELOW_FLOOR,
ALREADY_LISTED,
NONE,
}
const getWarningMessage = (warning: WarningType) => {
let message = <></>
switch (warning) {
@@ -91,7 +93,6 @@ interface PriceTextInputProps {
isGlobalPrice: boolean
setGlobalOverride: Dispatch<boolean>
globalOverride: boolean
warning?: ListingWarning
asset: WalletAsset
}
@@ -101,43 +102,35 @@ export const PriceTextInput = ({
isGlobalPrice,
setGlobalOverride,
globalOverride,
warning,
asset,
}: PriceTextInputProps) => {
const [focused, setFocused] = useState(false)
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()
useEffect(() => {
inputRef.current.value = listPrice !== undefined ? `${listPrice}` : ''
setWarningType(WarningType.NONE)
if (!warning && listPrice) {
if (listPrice < (asset?.floorPrice ?? 0)) setWarningType(WarningType.BELOW_FLOOR)
else if (asset.floor_sell_order_price && listPrice >= asset.floor_sell_order_price)
setWarningType(WarningType.ALREADY_LISTED)
} else if (warning && listPrice && listPrice >= 0) removeMarketplaceWarning(asset, warning)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPrice])
const percentBelowFloor = (1 - (listPrice ?? 0) / (asset.floorPrice ?? 0)) * 100
const warningColor =
showResolveIssues && !listPrice
(showResolveIssues && !listPrice) ||
warningType === WarningType.ALREADY_LISTED ||
(warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20)
? colors.red400
: warningType !== WarningType.NONE && !focused
? (warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20) ||
warningType === WarningType.ALREADY_LISTED
? colors.red400
: theme.accentWarning
: isGlobalPrice
: warningType === WarningType.BELOW_FLOOR
? theme.accentWarning
: isGlobalPrice || !!listPrice
? theme.accentAction
: listPrice != null
? theme.textSecondary
: theme.accentAction
: theme.textSecondary
const setPrice = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!listPrice && event.target.value.includes('.') && parseFloat(event.target.value) === 0) {
return
}
const val = parseFloat(event.target.value)
setListPrice(isNaN(val) ? undefined : val)
}
useUpdateInputAndWarnings(setWarningType, inputRef, asset, listPrice)
return (
<PriceTextInputWrapper>
@@ -151,48 +144,36 @@ export const PriceTextInput = ({
placeholder="0"
backgroundColor="none"
width={{ sm: '54', md: '68' }}
onFocus={() => setFocused(true)}
onBlur={() => {
setFocused(false)
}}
ref={inputRef}
onChange={(v: FormEvent<HTMLInputElement>) => {
if (!listPrice && v.currentTarget.value.includes('.') && parseFloat(v.currentTarget.value) === 0) {
return
}
const val = parseFloat(v.currentTarget.value)
setListPrice(isNaN(val) ? undefined : val)
}}
onChange={setPrice}
/>
<CurrencyWrapper listPrice={listPrice}>&nbsp;ETH</CurrencyWrapper>
{(isGlobalPrice || globalOverride) && (
<GlobalPriceIcon onClick={() => setGlobalOverride(!globalOverride)}>
{globalOverride ? <AttachPriceIcon /> : <EditPriceIcon />}
{globalOverride ? <BrokenLinkIcon /> : <Link size={20} color={warningColor} />}
</GlobalPriceIcon>
)}
</InputWrapper>
<WarningMessage $color={warningColor}>
{warning
? warning.message
: warningType !== WarningType.NONE && (
<WarningRow>
<AlertTriangle height={16} width={16} color={warningColor} />
<span>
{warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `}
{getWarningMessage(warningType)}
&nbsp;
{warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`}
</span>
<WarningAction
onClick={() => {
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
setWarningType(WarningType.NONE)
}}
>
{warningType === WarningType.BELOW_FLOOR ? <Trans>Dismiss</Trans> : <Trans>Remove item</Trans>}
</WarningAction>
</WarningRow>
)}
{warningType !== WarningType.NONE && (
<WarningRow>
<AlertTriangle height={16} width={16} color={warningColor} />
<span>
{warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `}
{getWarningMessage(warningType)}
&nbsp;
{warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`}
</span>
<WarningAction
onClick={() => {
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
setWarningType(WarningType.NONE)
}}
>
{warningType === WarningType.BELOW_FLOOR ? <Trans>Dismiss</Trans> : <Trans>Remove item</Trans>}
</WarningAction>
</WarningRow>
)}
</WarningMessage>
</PriceTextInputWrapper>
)

View File

@@ -1,6 +1,7 @@
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import Row from 'components/Row'
import { getRoyalty } from 'nft/components/profile/list/utils'
import { ListingMarket, WalletAsset } from 'nft/types'
import { formatEth } from 'nft/utils'
import styled from 'styled-components/macro'
@@ -50,7 +51,7 @@ export const RoyaltyTooltip = ({
asset: WalletAsset
fees?: number
}) => {
const maxRoyalty = Math.max(...selectedMarkets.map((market) => market.royalty ?? 0))
const maxRoyalty = Math.max(...selectedMarkets.map((market) => getRoyalty(market, asset) ?? 0)).toFixed(2)
return (
<RoyaltyContainer>
{selectedMarkets.map((market) => (

View File

@@ -14,3 +14,16 @@ export const TitleRow = styled(Row)`
justify-content: space-between;
margin-bottom: 8px;
`
export enum SetPriceMethod {
SAME_PRICE,
FLOOR_PRICE,
LAST_PRICE,
CUSTOM,
}
export enum WarningType {
BELOW_FLOOR,
ALREADY_LISTED,
NONE,
}

View File

@@ -0,0 +1,196 @@
import type { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
import { SetPriceMethod, WarningType } from 'nft/components/profile/list/shared'
import { useNFTList, useSellAsset } from 'nft/hooks'
import { LOOKSRARE_MARKETPLACE_CONTRACT, X2Y2_TRANSFER_CONTRACT } from 'nft/queries'
import { OPENSEA_CROSS_CHAIN_CONDUIT } from 'nft/queries/openSea'
import { CollectionRow, ListingMarket, ListingRow, ListingStatus, WalletAsset } from 'nft/types'
import { approveCollection, LOOKS_RARE_CREATOR_BASIS_POINTS, signListing } from 'nft/utils/listNfts'
import { Dispatch, useEffect } from 'react'
import shallow from 'zustand/shallow'
export async function approveCollectionRow(
collectionRow: CollectionRow,
signer: JsonRpcSigner,
setCollectionStatusAndCallback: (
collection: CollectionRow,
status: ListingStatus,
callback?: () => Promise<void>
) => void
) {
const callback = () => approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback)
setCollectionStatusAndCallback(collectionRow, ListingStatus.SIGNING, callback)
const { marketplace, collectionAddress } = collectionRow
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const spender =
marketplace.name === 'OpenSea'
? OPENSEA_CROSS_CHAIN_CONDUIT
: marketplace.name === 'Rarible'
? LOOKSRARE_MARKETPLACE_CONTRACT
: marketplace.name === 'X2Y2'
? X2Y2_TRANSFER_CONTRACT
: addresses.TRANSFER_MANAGER_ERC721
!!collectionAddress &&
(await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
setCollectionStatusAndCallback(collectionRow, newStatus, callback)
))
}
export async function signListingRow(
listing: ListingRow,
signer: JsonRpcSigner,
provider: Web3Provider,
getLooksRareNonce: () => number,
setLooksRareNonce: (nonce: number) => void,
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void
) {
const looksRareNonce = getLooksRareNonce()
const callback = () => {
return signListingRow(listing, signer, provider, getLooksRareNonce, setLooksRareNonce, setListingStatusAndCallback)
}
setListingStatusAndCallback(listing, ListingStatus.SIGNING, callback)
const { asset, marketplace } = listing
const res = await signListing(marketplace, asset, signer, provider, looksRareNonce, (newStatus: ListingStatus) =>
setListingStatusAndCallback(listing, newStatus, callback)
)
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
}
export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
const total = sellAssets.reduce((total, asset: WalletAsset) => {
if (asset.newListings?.length) {
const maxListing = asset.newListings.reduce((a, b) => ((a.price ?? 0) > (b.price ?? 0) ? a : b))
// LooksRare is a unique case where creator royalties are a flat 0.5% or 50 basis points
const maxFee =
maxListing.marketplace.fee +
(maxListing.marketplace.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset?.basisPoints ?? 0) / 100
return total + (maxListing.price ?? 0) - (maxListing.price ?? 0) * (maxFee / 100)
}
return total
}, 0)
return total ? Math.round(total * 10000 + Number.EPSILON) / 10000 : 0
}
const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]] => {
const newCollectionsToApprove: CollectionRow[] = []
const newListings: ListingRow[] = []
sellAssets.forEach((asset) => {
asset.marketplaces?.forEach((marketplace: ListingMarket) => {
const newListing = {
images: [asset.smallImageUrl, marketplace.icon],
name: asset.name || `#${asset.tokenId}`,
status: ListingStatus.DEFINED,
asset,
marketplace,
price: asset.newListings?.find((listing) => listing.marketplace.name === marketplace.name)?.price,
}
newListings.push(newListing)
if (
!newCollectionsToApprove.some(
(collectionRow: CollectionRow) =>
collectionRow.collectionAddress === asset.asset_contract.address &&
collectionRow.marketplace.name === marketplace.name
)
) {
const newCollectionRow = {
images: [asset.asset_contract.image_url, marketplace.icon],
name: asset.asset_contract.name,
status: ListingStatus.DEFINED,
collectionAddress: asset.asset_contract.address,
isVerified: asset.collectionIsVerified,
marketplace,
}
newCollectionsToApprove.push(newCollectionRow)
}
})
})
return [newCollectionsToApprove, newListings]
}
export const verifyStatus = (status: ListingStatus) => {
return status !== ListingStatus.PAUSED && status !== ListingStatus.APPROVED
}
export function useSubscribeListingState() {
const sellAssets = useSellAsset((state) => state.sellAssets)
const { setListings, setCollectionsRequiringApproval } = useNFTList(
({ setListings, setCollectionsRequiringApproval }) => ({
setListings,
setCollectionsRequiringApproval,
}),
shallow
)
useEffect(() => {
const [newCollectionsToApprove, newListings] = getListings(sellAssets)
setListings(newListings)
setCollectionsRequiringApproval(newCollectionsToApprove)
}, [sellAssets, setCollectionsRequiringApproval, setListings])
}
export function useHandleGlobalPriceToggle(
globalOverride: boolean,
setListPrice: Dispatch<number | undefined>,
setPrice: (price?: number) => void,
listPrice?: number,
globalPrice?: number
) {
useEffect(() => {
let price: number | undefined
if (globalOverride) {
if (!listPrice) setListPrice(globalPrice)
price = globalPrice
} else {
price = listPrice
}
setPrice(price)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalOverride])
}
export function useSyncPriceWithGlobalMethod(
asset: WalletAsset,
setListPrice: Dispatch<number | undefined>,
setGlobalPrice: Dispatch<number | undefined>,
setGlobalOverride: Dispatch<boolean>,
listPrice?: number,
globalPrice?: number,
globalPriceMethod?: SetPriceMethod
) {
useEffect(() => {
if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) {
setListPrice(asset?.floorPrice)
setGlobalPrice(asset.floorPrice)
} else if (globalPriceMethod === SetPriceMethod.LAST_PRICE) {
setListPrice(asset.lastPrice)
setGlobalPrice(asset.lastPrice)
} else if (globalPriceMethod === SetPriceMethod.SAME_PRICE)
listPrice && !globalPrice ? setGlobalPrice(listPrice) : setListPrice(globalPrice)
setGlobalOverride(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalPriceMethod])
}
export function useUpdateInputAndWarnings(
setWarningType: Dispatch<WarningType>,
inputRef: React.MutableRefObject<HTMLInputElement>,
asset: WalletAsset,
listPrice?: number
) {
useEffect(() => {
setWarningType(WarningType.NONE)
const price = listPrice ?? 0
inputRef.current.value = `${price}`
if (price < (asset?.floorPrice ?? 0) && price > 0) setWarningType(WarningType.BELOW_FLOOR)
else if (asset.floor_sell_order_price && price >= asset.floor_sell_order_price)
setWarningType(WarningType.ALREADY_LISTED)
}, [asset?.floorPrice, asset.floor_sell_order_price, inputRef, listPrice, setWarningType])
}
export const getRoyalty = (listingMarket: ListingMarket, asset: WalletAsset) => {
// LooksRare is a unique case where royalties for creators are a flat 0.5% or 50 basis points
const baseFee = listingMarket.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset.basisPoints ?? 0
return baseFee * 0.01
}

View File

@@ -138,7 +138,7 @@ export const ProfilePage = () => {
borderRadius="12"
paddingX="16"
paddingY="12"
background="backgroundModule"
background="backgroundSurface"
borderStyle="solid"
borderColor="backgroundOutline"
borderWidth="1px"

View File

@@ -18,27 +18,10 @@ export const subheadSmall = sprinkles({ fontWeight: 'medium', fontSize: '14', li
export const body = sprinkles({ fontWeight: 'normal', fontSize: '16', lineHeight: '24' })
export const bodySmall = sprinkles({ fontWeight: 'normal', fontSize: '14', lineHeight: '20' })
export const caption = sprinkles({ fontWeight: 'normal', fontSize: '12', lineHeight: '16' })
export const badge = sprinkles({ fontWeight: 'semibold', fontSize: '10', lineHeight: '12' })
export const buttonTextMedium = sprinkles({ fontWeight: 'semibold', fontSize: '16', lineHeight: '20' })
export const buttonTextSmall = sprinkles({ fontWeight: 'semibold', fontSize: '14', lineHeight: '16' })
export const commonButtonStyles = style([
sprinkles({
borderRadius: '12',
transition: '250',
}),
{
border: 'none',
':hover': {
cursor: 'pointer',
},
':disabled': {
cursor: 'auto',
},
},
])
const magicalGradient = style({
selectors: {
'&::before': {

View File

@@ -4,12 +4,10 @@ import { devtools } from 'zustand/middleware'
interface NFTListState {
looksRareNonce: number
listingStatus: ListingStatus
listings: ListingRow[]
collectionsRequiringApproval: CollectionRow[]
setLooksRareNonce: (nonce: number) => void
getLooksRareNonce: () => number
setListingStatus: (status: ListingStatus) => void
setListings: (listings: ListingRow[]) => void
setCollectionsRequiringApproval: (collections: CollectionRow[]) => void
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void
@@ -23,7 +21,6 @@ interface NFTListState {
export const useNFTList = create<NFTListState>()(
devtools((set, get) => ({
looksRareNonce: 0,
listingStatus: ListingStatus.DEFINED,
listings: [],
collectionsRequiringApproval: [],
setLooksRareNonce: (nonce) =>
@@ -33,10 +30,6 @@ export const useNFTList = create<NFTListState>()(
getLooksRareNonce: () => {
return get().looksRareNonce
},
setListingStatus: (status) =>
set(() => {
return { listingStatus: status }
}),
setListings: (listings) =>
set(() => {
const updatedListings = listings.map((listing) => {

View File

@@ -1,7 +1,7 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { ListingMarket, ListingWarning, WalletAsset } from '../types'
import { ListingMarket, WalletAsset } from '../types'
interface SellAssetState {
sellAssets: WalletAsset[]
@@ -16,10 +16,6 @@ interface SellAssetState {
removeAssetMarketplace: (asset: WalletAsset, marketplace: ListingMarket) => void
toggleShowResolveIssues: () => void
setIssues: (issues: number) => void
// TODO: After merging v2, see if this marketplace logic can be removed
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
removeAllMarketplaceWarnings: () => void
}
export const useSellAsset = create<SellAssetState>()(
@@ -118,47 +114,6 @@ export const useSellAsset = create<SellAssetState>()(
return { sellAssets: assetsCopy }
})
},
addMarketplaceWarning: (asset, warning) => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
asset.listingWarnings?.push(warning)
const index = sellAssets.findIndex(
(n) => n.tokenId === asset.tokenId && n.asset_contract.address === asset.asset_contract.address
)
assetsCopy[index] = asset
return { sellAssets: assetsCopy }
})
},
removeMarketplaceWarning: (asset, warning, setGlobalOverride?) => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
if (asset.listingWarnings === undefined || asset.newListings === undefined) return { sellAssets: assetsCopy }
const warningIndex =
asset.listingWarnings?.findIndex((n) => n.marketplace.name === warning.marketplace.name) ?? -1
asset.listingWarnings?.splice(warningIndex, 1)
if (warning?.message?.includes('LISTING BELOW FLOOR')) {
if (setGlobalOverride) {
asset.newListings?.forEach((listing) => (listing.overrideFloorPrice = true))
} else {
const listingIndex =
asset.newListings?.findIndex((n) => n.marketplace.name === warning.marketplace.name) ?? -1
asset.newListings[listingIndex].overrideFloorPrice = true
}
}
const index = sellAssets.findIndex(
(n) => n.tokenId === asset.tokenId && n.asset_contract.address === asset.asset_contract.address
)
assetsCopy[index] = asset
return { sellAssets: assetsCopy }
})
},
removeAllMarketplaceWarnings: () => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
assetsCopy.map((asset) => (asset.listingWarnings = []))
return { sellAssets: assetsCopy }
})
},
toggleShowResolveIssues: () => {
set(({ showResolveIssues }) => {
return { showResolveIssues: !showResolveIssues }

View File

@@ -15,6 +15,7 @@ import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPage
import { BagCloseIcon } from 'nft/components/icons'
import { useBag, useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks'
import * as styles from 'nft/pages/collection/index.css'
import { blocklistedCollections } from 'nft/utils'
import { Suspense, useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { animated, easings, useSpring } from 'react-spring'
@@ -85,7 +86,7 @@ const FiltersContainer = styled.div<{ isMobile: boolean; isFiltersExpanded: bool
width: ${({ isMobile }) => (isMobile ? '100%' : '0px')};
height: ${({ isMobile, isFiltersExpanded }) => (isMobile && isFiltersExpanded ? '100%' : undefined)};
background: ${({ theme, isMobile }) => (isMobile ? theme.backgroundBackdrop : undefined)};
z-index: ${Z_INDEX.modalBackdrop};
z-index: ${Z_INDEX.modalBackdrop - 3};
overflow-y: ${({ isMobile }) => (isMobile ? 'scroll' : undefined)};
@media screen and (min-width: ${({ theme }) => theme.breakpoint.sm}px) {
@@ -188,7 +189,7 @@ const Collection = () => {
width: CollectionContainerWidthChange.to((x) => `calc(100% - ${x as number}px)`),
}}
>
{contractAddress ? (
{contractAddress && !blocklistedCollections.includes(contractAddress) ? (
<>
<BannerWrapper>
<Banner

View File

@@ -1,14 +1,14 @@
import { Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics'
import { InterfacePageName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { ButtonPrimary } from 'components/Button'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { XXXL_BAG_WIDTH } from 'nft/components/bag/Bag'
import { ListPage } from 'nft/components/profile/list/ListPage'
import { ProfilePage } from 'nft/components/profile/view/ProfilePage'
import { ProfilePageLoadingSkeleton } from 'nft/components/profile/view/ProfilePageLoadingSkeleton'
import { useBag, useNFTList, useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { ListingStatus, ProfilePageStateType } from 'nft/types'
import { useBag, useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { ProfilePageStateType } from 'nft/types'
import { Suspense, useEffect, useRef } from 'react'
import { useToggleWalletModal } from 'state/application/hooks'
import styled from 'styled-components/macro'
@@ -26,17 +26,17 @@ const ProfilePageWrapper = styled.div`
}
`
const LoadedAccountPage = styled.div<{ cartExpanded: boolean; isOnV2ListPage: boolean }>`
const LoadedAccountPage = styled.div<{ cartExpanded: boolean; isListingNfts: boolean }>`
width: calc(
100% -
${({ cartExpanded, isOnV2ListPage }) =>
isOnV2ListPage ? LIST_PAGE_MARGIN * 2 : cartExpanded ? XXXL_BAG_WIDTH : 0}px
${({ cartExpanded, isListingNfts }) =>
isListingNfts ? LIST_PAGE_MARGIN * 2 : cartExpanded ? XXXL_BAG_WIDTH : 0}px
);
margin: 0px ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN : 0)}px;
margin: 0px ${({ isListingNfts }) => (isListingNfts ? LIST_PAGE_MARGIN : 0)}px;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: calc(100% - ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_MOBILE * 2 : 0)}px);
margin: 0px ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_MOBILE : 0)}px;
width: calc(100% - ${({ isListingNfts }) => (isListingNfts ? LIST_PAGE_MARGIN_MOBILE * 2 : 0)}px);
margin: 0px ${({ isListingNfts }) => (isListingNfts ? LIST_PAGE_MARGIN_MOBILE : 0)}px;
}
`
@@ -62,15 +62,8 @@ const ConnectWalletButton = styled(ButtonPrimary)`
const ProfileContent = () => {
const sellPageState = useProfilePageState((state) => state.state)
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
const removeAllMarketplaceWarnings = useSellAsset((state) => state.removeAllMarketplaceWarnings)
const resetSellAssets = useSellAsset((state) => state.reset)
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
const setListingStatus = useNFTList((state) => state.setListingStatus)
useEffect(() => {
removeAllMarketplaceWarnings()
setListingStatus(ListingStatus.DEFINED)
}, [removeAllMarketplaceWarnings, sellPageState, setListingStatus])
const { account } = useWeb3React()
const accountRef = useRef(account)
@@ -85,25 +78,23 @@ const ProfileContent = () => {
}
}, [account, resetSellAssets, setSellPageState, clearCollectionFilters])
const cartExpanded = useBag((state) => state.bagExpanded)
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const isListingNfts = sellPageState === ProfilePageStateType.LISTING
const isOnV2ListPage = isNftListV2 && isListingNfts
return (
<Trace page={InterfacePageName.NFT_PROFILE_PAGE} shouldLogImpression>
<ProfilePageWrapper>
{account ? (
<LoadedAccountPage cartExpanded={cartExpanded} isOnV2ListPage={isOnV2ListPage}>
<LoadedAccountPage cartExpanded={cartExpanded} isListingNfts={isListingNfts}>
{!isListingNfts ? <ProfilePage /> : <ListPage />}
</LoadedAccountPage>
) : (
<Center>
<ThemedText.HeadlineMedium lineHeight="36px" color="textSecondary" fontWeight="600" marginBottom="24px">
No items to display
<Trans>No items to display</Trans>
</ThemedText.HeadlineMedium>
<ConnectWalletButton onClick={toggleWalletModal}>
<ThemedText.SubHeader color="white" lineHeight="20px">
Connect Wallet
<Trans>Connect Wallet</Trans>
</ThemedText.SubHeader>
</ConnectWalletButton>
</Center>

View File

@@ -1,4 +1,5 @@
import { isAddress } from '@ethersproject/address'
import { blocklistedCollections } from 'nft/utils'
import { GenieCollection } from '../../types'
@@ -45,15 +46,17 @@ export const fetchSearchCollections = async (addressOrName: string, recursive =
if (isName) {
const data = await r.json()
const formattedData = data?.data
? data.data.map((collection: { stats: Record<string, unknown>; floorPrice: string }) => {
return {
...collection,
stats: {
...collection.stats,
floor_price: collection.floorPrice,
},
}
})
? data.data
.filter((collection: { address: string }) => !blocklistedCollections.includes(collection.address))
.map((collection: { stats: Record<string, unknown>; floorPrice: string }) => {
return {
...collection,
stats: {
...collection.stats,
floor_price: collection.floorPrice,
},
}
})
: []
return formattedData.slice(0, MAX_SEARCH_RESULTS)
}

View File

@@ -1,3 +1,5 @@
import { blocklistedCollections } from 'nft/utils'
import { TimePeriod, TrendingCollection } from '../../types'
const NFT_API_URL = process.env.REACT_APP_TEMP_API_URL
@@ -18,5 +20,5 @@ export const fetchTrendingCollections = async (payload: {
const data = await r.json()
return data ?? []
return data.filter((collection: { address: string }) => !blocklistedCollections.includes(collection.address)) ?? []
}

View File

@@ -6,11 +6,6 @@ export interface ListingMarket {
name: string
fee: number
icon: string
royalty?: number
}
export interface ListingWarning {
marketplace: ListingMarket
message: string
}
export interface SellOrder {
@@ -72,7 +67,6 @@ export interface WalletAsset {
marketAgnosticPrice?: number
newListings?: Listing[]
marketplaces?: ListingMarket[]
listingWarnings?: ListingWarning[]
}
export interface WalletCollection {

View File

@@ -22,3 +22,8 @@ export const isInSameSudoSwapPool = (assetA: GenieAsset, assetB: GenieAsset): bo
export const isInSameMarketplaceCollection = (assetA: GenieAsset, assetB: GenieAsset): boolean => {
return assetA.address === assetB.address && assetA.marketplace === assetB.marketplace
}
export const blocklistedCollections = [
'0xd5eeac01b0d1d929d6cffaaf78020af137277293',
'0x85c08fffa9510f87019efdcf986301873cbb10d6',
]

View File

@@ -58,11 +58,13 @@ const Profile = lazy(() => import('nft/pages/profile/profile'))
const Asset = lazy(() => import('nft/pages/asset/Asset'))
// Placeholder API key. Actual API key used in the proxy server
const ANALYTICS_DUMMY_KEY = '00000000000000000000000000000000'
const ANALYTICS_PROXY_URL = process.env.REACT_APP_AMPLITUDE_PROXY_URL
const AMPLITUDE_DUMMY_KEY = '00000000000000000000000000000000'
const AMPLITUDE_PROXY_URL = process.env.REACT_APP_AMPLITUDE_PROXY_URL
const STATSIG_DUMMY_KEY = 'client-0000000000000000000000000000000000000000000'
const STATSIG_PROXY_URL = process.env.REACT_APP_STATSIG_PROXY_URL
const COMMIT_HASH = process.env.REACT_APP_GIT_COMMIT_HASH
initializeAnalytics(ANALYTICS_DUMMY_KEY, OriginApplication.INTERFACE, {
proxyUrl: ANALYTICS_PROXY_URL,
initializeAnalytics(AMPLITUDE_DUMMY_KEY, OriginApplication.INTERFACE, {
proxyUrl: AMPLITUDE_PROXY_URL,
defaultEventName: SharedEventName.PAGE_VIEWED,
commitHash: COMMIT_HASH,
isProductionEnv: isProductionEnv(),
@@ -210,10 +212,11 @@ export default function App() {
<StatsigProvider
user={statsigUser}
// TODO: replace with proxy and cycle key
sdkKey={process.env.REACT_APP_STATSIG_API_KEY ?? ''}
sdkKey={STATSIG_DUMMY_KEY}
waitForInitialization={false}
options={{
environment: { tier: getEnvName() },
api: STATSIG_PROXY_URL,
}}
>
<HeaderWrapper transparent={isHeaderTransparent}>

View File

@@ -116,7 +116,7 @@ export default function MigrateV2() {
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<BackArrow to="/pool/v2" />
<BackArrow to="/pool" />
<ThemedText.DeprecatedMediumHeader>
<Trans>Migrate V2 Liquidity</Trans>
</ThemedText.DeprecatedMediumHeader>

View File

@@ -1,14 +1,18 @@
import { DEFAULT_LIST_OF_LISTS } from './../constants/lists'
const DEFAULT_LIST_PRIORITIES = DEFAULT_LIST_OF_LISTS.reduce<{ [listUrl: string]: number }>((memo, listUrl, index) => {
memo[listUrl] = index + 1
return memo
}, {})
const DEFAULT_LIST_PRIORITIES = DEFAULT_LIST_OF_LISTS.reduce(
(acc, listUrl, index) => ({
...acc,
[listUrl]: index + 1,
}),
{}
) as Record<string, number>
// use ordering of default list of lists to assign priority
export default function sortByListPriority(urlA: string, urlB: string) {
if (DEFAULT_LIST_PRIORITIES[urlA] && DEFAULT_LIST_PRIORITIES[urlB]) {
return DEFAULT_LIST_PRIORITIES[urlA] - DEFAULT_LIST_PRIORITIES[urlB]
}
return 0
const A = DEFAULT_LIST_PRIORITIES[urlA]
const B = DEFAULT_LIST_PRIORITIES[urlB]
if (!A) return 0
if (!B) return 0
return A - B
}

View File

@@ -4,6 +4,9 @@ import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
export function getNativeTokenDBAddress(chain: Chain): string | undefined {
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
if (pageChainId === undefined) {
return undefined
}
switch (chain) {
case Chain.Celo:
case Chain.Polygon:

Binary file not shown.

408
yarn.lock
View File

@@ -1156,12 +1156,12 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@>=7.17.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.19.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78"
integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==
"@babel/runtime@>=7.17.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
dependencies:
regenerator-runtime "^0.13.4"
regenerator-runtime "^0.13.11"
"@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.15.4", "@babel/template@^7.16.7", "@babel/template@^7.18.10", "@babel/template@^7.3.3":
version "7.18.10"
@@ -1210,25 +1210,26 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@coinbase/wallet-sdk@^3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-3.3.0.tgz#e77383bce1388e5367dc35f8309702e73be47503"
integrity sha512-Prmxs5eYRxe5i+kDSsny97oPG4Pa5PhLmNDx8f7UQrvlPowGy5Tg0gHOqCie6ck2shVMdW8sKJ+RCLIRZ9kIjA==
"@coinbase/wallet-sdk@^3.6.4":
version "3.6.4"
resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-3.6.4.tgz#00b4325c501ec5cdd07ac1b365ab226cb3df3a22"
integrity sha512-2ecCT0/pmaMNVyMF7J1ZLFTfLnpnrHNQOGyIcbMBIepeqlE3jndjU023OdwwVLrLXyvfyelJ8K1iwAOvyEZxUw==
dependencies:
"@metamask/safe-event-emitter" "2.0.0"
"@solana/web3.js" "^1.70.1"
bind-decorator "^1.0.11"
bn.js "^5.1.1"
buffer "^6.0.3"
clsx "^1.1.0"
eth-block-tracker "4.4.3"
eth-json-rpc-filters "4.2.2"
eth-json-rpc-filters "5.1.0"
eth-rpc-errors "4.0.2"
js-sha256 "0.9.0"
json-rpc-engine "6.1.0"
keccak "^3.0.1"
preact "^10.5.9"
qs "^6.10.3"
rxjs "^6.6.3"
sha.js "^2.4.11"
stream-browserify "^3.0.0"
util "^0.12.4"
@@ -3251,6 +3252,21 @@
resolved "https://registry.npmjs.org/@multiformats/base-x/-/base-x-4.0.1.tgz"
integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==
"@noble/ed25519@^1.7.0":
version "1.7.3"
resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123"
integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==
"@noble/hashes@^1.1.2":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12"
integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==
"@noble/secp256k1@^1.6.3":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -3680,6 +3696,35 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@solana/buffer-layout@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15"
integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==
dependencies:
buffer "~6.0.3"
"@solana/web3.js@^1.70.1":
version "1.73.3"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.73.3.tgz#60e6bd68f6f364d4be360b1e0a03a0a68468a029"
integrity sha512-vHRMo589XEIpoujpE2sZZ1aMZvfA1ImKfNxobzEFyMb+H5j6mRRUXfdgWD0qJ0sm11e5BcBC7HPeRXJB+7f3Lg==
dependencies:
"@babel/runtime" "^7.12.5"
"@noble/ed25519" "^1.7.0"
"@noble/hashes" "^1.1.2"
"@noble/secp256k1" "^1.6.3"
"@solana/buffer-layout" "^4.0.0"
agentkeepalive "^4.2.1"
bigint-buffer "^1.1.5"
bn.js "^5.0.0"
borsh "^0.7.0"
bs58 "^4.0.1"
buffer "6.0.1"
fast-stable-stringify "^1.0.0"
jayson "^3.4.4"
node-fetch "^2.6.7"
rpc-websockets "^7.5.1"
superstruct "^0.14.2"
"@styled-system/background@^5.1.2":
version "5.1.2"
resolved "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz"
@@ -4025,13 +4070,6 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/bn.js@^4.11.3":
version "4.11.6"
resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz"
integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==
dependencies:
"@types/node" "*"
"@types/bn.js@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68"
@@ -4039,6 +4077,13 @@
dependencies:
"@types/node" "*"
"@types/connect@^3.4.33":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
dependencies:
"@types/node" "*"
"@types/d3-array@^2":
version "2.12.3"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.12.3.tgz#8d16d51fb04ad5a5a8ebe14eb8263a579f1efdd1"
@@ -4444,6 +4489,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
"@types/node@^12.12.54":
version "12.20.55"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
"@types/node@^13.13.5":
version "13.13.52"
resolved "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz"
@@ -4708,6 +4758,13 @@
anymatch "^3.0.0"
source-map "^0.6.0"
"@types/ws@^7.4.4":
version "7.4.7"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==
dependencies:
"@types/node" "*"
"@types/ws@^8.0.0":
version "8.5.3"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
@@ -4931,15 +4988,15 @@
"@typescript-eslint/types" "5.47.0"
eslint-visitor-keys "^3.3.0"
"@uniswap/analytics-events@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-2.3.0.tgz#e4d6b29633c09872be3b9b760b1a192b96368887"
integrity sha512-oShunkYEfa45RQAtl2aQfF91gfX4QirLa/fR+FyL5jfl+Ei4AZ1ihtyVjJ1VLOJlObX1p08JjlpA0yxqDwPYHw==
"@uniswap/analytics-events@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-2.4.0.tgz#910f727bf4c72f5d1890f17daec4687dc44ee6c0"
integrity sha512-oZl2KRCSTAO8C3sxEFX6BEdGujdPs8tk/Zyx7UwOZOUDcim3mxx6rsdRvnwsAgkZAJjV9I1HSUR7JEeJmQkRmQ==
"@uniswap/analytics@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@uniswap/analytics/-/analytics-1.3.0.tgz#42de08949a7d529ebba647d76801f143792c132a"
integrity sha512-cwx3HDxcqehr5uUnnAJ20lak9jA68e+l8ww/s4XxoJzedkdHz0TWXc4+ZZ2iukKEun4oU/d3clrU6u3Cu6xDpg==
"@uniswap/analytics@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@uniswap/analytics/-/analytics-1.3.1.tgz#086a681fc483c08e1b3d896cce287bc162ad0c1d"
integrity sha512-wB8J+e9oeu+VSx3tgfA304g7MKf6zuAesFeEGKdD1pI2LNOoMQHczbnf8I8lPjPYnXxLvdbGVKRQZXWaI+jTGw==
dependencies:
"@amplitude/analytics-browser" "^1.5.8"
react "^18.2.0"
@@ -5194,10 +5251,10 @@
"@uniswap/v3-core" "1.0.0"
"@uniswap/v3-periphery" "^1.0.1"
"@uniswap/widgets@^2.29.3":
version "2.29.3"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.29.3.tgz#1b2f0d4f7f4db16d6d12f40204bcee6a67e46c1c"
integrity sha512-gsyiNxeqcj5Wn5IsaAKIz9M1v9lkmYKGxCrJHWADw8UyuTrPnL7qXg6GkBRR5kYkd+eeK6lK28qc1BEzl7dvmQ==
"@uniswap/widgets@^2.40.0":
version "2.40.0"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.40.0.tgz#98539f31cc21a8e70ced86da65db642f3380db14"
integrity sha512-55I6r8a3XfWAEFfU6eAj4BUbaYRBPyrbfzaedAy0+9SAntnxxT7wyh3DMC9MoMLEwd4PPLemWyLfVQTVIoYn0g==
dependencies:
"@babel/runtime" ">=7.17.0"
"@fontsource/ibm-plex-mono" "^4.5.1"
@@ -6047,6 +6104,14 @@
resolved "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz"
integrity sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg==
JSONStream@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
dependencies:
jsonparse "^1.2.0"
through ">=2.2.7 <3"
abab@^2.0.3, abab@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
@@ -6135,6 +6200,15 @@ agent-base@6:
dependencies:
debug "4"
agentkeepalive@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717"
integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==
dependencies:
debug "^4.1.0"
depd "^1.1.2"
humanize-ms "^1.2.1"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@@ -6940,6 +7014,13 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bigint-buffer@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442"
integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==
dependencies:
bindings "^1.3.0"
bignumber.js@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-8.1.1.tgz#4b072ae5aea9c20f6730e4e5d529df1271c4d885"
@@ -6965,9 +7046,9 @@ bind-decorator@^1.0.11:
resolved "https://registry.npmjs.org/bind-decorator/-/bind-decorator-1.0.11.tgz"
integrity sha1-5BvAah9l3ZzsR2yRxdrzl4SIJS8=
bindings@^1.5.0:
bindings@^1.3.0, bindings@^1.5.0:
version "1.5.0"
resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
dependencies:
file-uri-to-path "1.0.0"
@@ -7011,7 +7092,7 @@ bn.js@4.11.8:
resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz"
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9:
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
@@ -7054,6 +7135,15 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
borsh@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a"
integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==
dependencies:
bn.js "^5.2.0"
bs58 "^4.0.0"
text-encoding-utf-8 "^1.0.2"
boxen@1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz"
@@ -7203,10 +7293,10 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.19.1, browserslist@^4
node-releases "^2.0.6"
update-browserslist-db "^1.0.9"
bs58@^4.0.0:
bs58@^4.0.0, bs58@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz"
integrity sha1-vhYedsNU9veIrkBx9j806MTwpCo=
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==
dependencies:
base-x "^3.0.2"
@@ -7226,11 +7316,6 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
btoa@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz"
integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz"
@@ -7284,6 +7369,14 @@ buffer-xor@^1.0.3:
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
buffer@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.1.tgz#3cbea8c1463e5a0779e30b66d4c88c6ffa182ac2"
integrity sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
buffer@^4.3.0:
version "4.9.2"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
@@ -7301,7 +7394,7 @@ buffer@^5.2.0, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
buffer@^6.0.3:
buffer@^6.0.3, buffer@~6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
@@ -7309,10 +7402,10 @@ buffer@^6.0.3:
base64-js "^1.3.1"
ieee754 "^1.2.1"
bufferutil@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.6.tgz#ebd6c67c7922a0e902f053e5d8be5ec850e48433"
integrity sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==
bufferutil@^4.0.1, bufferutil@^4.0.6:
version "4.0.7"
resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad"
integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==
dependencies:
node-gyp-build "^4.3.0"
@@ -7842,7 +7935,7 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
clone@2.x, clone@^2.1.1:
clone@2.x:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
@@ -7986,7 +8079,7 @@ commander@7:
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^2.20.0:
commander@^2.20.0, commander@^2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -9184,15 +9277,20 @@ delaunator@5:
dependencies:
robust-predicates "^3.0.0"
delay@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d"
integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
depd@~1.1.2:
depd@^1.1.2, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
dependency-graph@^0.11.0:
version "0.11.0"
@@ -9690,6 +9788,18 @@ es6-iterator@2.0.3, es6-iterator@~2.0.3:
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-promise@^4.0.3:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
es6-promisify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==
dependencies:
es6-promise "^4.0.3"
es6-symbol@^3.1.1, es6-symbol@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
@@ -10080,35 +10190,17 @@ eth-block-tracker@4.4.3:
pify "^3.0.0"
safe-event-emitter "^1.0.1"
eth-json-rpc-filters@4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-4.2.2.tgz#eb35e1dfe9357ace8a8908e7daee80b2cd60a10d"
integrity sha512-DGtqpLU7bBg63wPMWg1sCpkKCf57dJ+hj/k3zF26anXMzkmtSBDExL8IhUu7LUd34f0Zsce3PYNO2vV2GaTzaw==
eth-json-rpc-filters@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-5.1.0.tgz#f0c2aeaec2a45e2dc6ca1b9843d8e85447821427"
integrity sha512-fos+9xmoa1A2Ytsc9eYof17r81BjdJOUcGcgZn4K/tKdCCTb+a8ytEtwlu1op5qsXFDlgGmstTELFrDEc89qEQ==
dependencies:
"@metamask/safe-event-emitter" "^2.0.0"
async-mutex "^0.2.6"
eth-json-rpc-middleware "^6.0.0"
eth-query "^2.1.2"
json-rpc-engine "^6.1.0"
pify "^5.0.0"
eth-json-rpc-middleware@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-6.0.0.tgz"
integrity sha512-qqBfLU2Uq1Ou15Wox1s+NX05S9OcAEL4JZ04VZox2NS0U+RtCMjSxzXhLFWekdShUPZ+P8ax3zCO2xcPrp6XJQ==
dependencies:
btoa "^1.2.1"
clone "^2.1.1"
eth-query "^2.1.2"
eth-rpc-errors "^3.0.0"
eth-sig-util "^1.4.2"
ethereumjs-util "^5.1.2"
json-rpc-engine "^5.3.0"
json-stable-stringify "^1.0.1"
node-fetch "^2.6.1"
pify "^3.0.0"
safe-event-emitter "^1.0.1"
eth-query@^2.1.0, eth-query@^2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz"
@@ -10124,21 +10216,6 @@ eth-rpc-errors@4.0.2, eth-rpc-errors@^4.0.2:
dependencies:
fast-safe-stringify "^2.0.6"
eth-rpc-errors@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz"
integrity sha512-iPPNHPrLwUlR9xCSYm7HHQjWBasor3+KZfRvwEWxMz3ca0yqnlBeJrnyphkGIXZ4J7AMAaOLmwy4AWhnxOiLxg==
dependencies:
fast-safe-stringify "^2.0.6"
eth-sig-util@^1.4.2:
version "1.4.2"
resolved "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz"
integrity sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=
dependencies:
ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git"
ethereumjs-util "^5.1.1"
ethereum-bloom-filters@^1.0.6:
version "1.0.10"
resolved "https://registry.yarnpkg.com/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz#3ca07f4aed698e75bd134584850260246a5fed8a"
@@ -10167,39 +10244,6 @@ ethereum-cryptography@^0.1.3:
secp256k1 "^4.0.1"
setimmediate "^1.0.5"
"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git":
version "0.6.8"
resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee3994657fa7a427238e6ba92a84d0b529bbcde0"
dependencies:
bn.js "^4.11.8"
ethereumjs-util "^6.0.0"
ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2:
version "5.2.1"
resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz"
integrity sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==
dependencies:
bn.js "^4.11.0"
create-hash "^1.1.2"
elliptic "^6.5.2"
ethereum-cryptography "^0.1.3"
ethjs-util "^0.1.3"
rlp "^2.0.0"
safe-buffer "^5.1.1"
ethereumjs-util@^6.0.0:
version "6.2.1"
resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz"
integrity sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==
dependencies:
"@types/bn.js" "^4.11.3"
bn.js "^4.11.0"
create-hash "^1.1.2"
elliptic "^6.5.2"
ethereum-cryptography "^0.1.3"
ethjs-util "0.1.6"
rlp "^2.2.3"
ethereumjs-util@^7.1.0:
version "7.1.5"
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181"
@@ -10255,14 +10299,6 @@ ethjs-unit@0.1.6:
bn.js "4.11.6"
number-to-bn "1.7.0"
ethjs-util@0.1.6, ethjs-util@^0.1.3:
version "0.1.6"
resolved "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz"
integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==
dependencies:
is-hex-prefixed "1.0.0"
strip-hex-prefix "1.0.0"
eval@0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.6.tgz#9620d7d8c85515e97e6b47c5814f46ae381cb3cc"
@@ -10523,6 +10559,11 @@ extsprintf@1.3.0, extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
eyes@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz"
@@ -10564,6 +10605,11 @@ fast-safe-stringify@^2.0.6:
resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz"
integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
fast-stable-stringify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313"
integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==
fast-url-parser@1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz"
@@ -11676,6 +11722,13 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
dependencies:
ms "^2.0.0"
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz"
@@ -12483,6 +12536,11 @@ isomorphic-ws@5.0.0:
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf"
integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==
isomorphic-ws@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -12539,6 +12597,25 @@ javascript-stringify@^2.0.1:
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79"
integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==
jayson@^3.4.4:
version "3.7.0"
resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.7.0.tgz#b735b12d06d348639ae8230d7a1e2916cb078f25"
integrity sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==
dependencies:
"@types/connect" "^3.4.33"
"@types/node" "^12.12.54"
"@types/ws" "^7.4.4"
JSONStream "^1.3.5"
commander "^2.20.3"
delay "^5.0.0"
es6-promisify "^5.0.0"
eyes "^0.1.8"
isomorphic-ws "^4.0.1"
json-stringify-safe "^5.0.1"
lodash "^4.17.20"
uuid "^8.3.2"
ws "^7.4.5"
jest-changed-files@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0"
@@ -13019,7 +13096,7 @@ js-base64@^3.7.2:
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745"
integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==
js-sha256@0.9.0, js-sha256@^0.9.0:
js-sha256@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==
@@ -13120,14 +13197,6 @@ json-rpc-engine@6.1.0, json-rpc-engine@^6.1.0:
"@metamask/safe-event-emitter" "^2.0.0"
eth-rpc-errors "^4.0.2"
json-rpc-engine@^5.3.0:
version "5.4.0"
resolved "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-5.4.0.tgz"
integrity sha512-rAffKbPoNDjuRnXkecTjnsE3xLLrb00rEkdgalINhaYVYIxDwWtvYBr9UFbhTvPB1B2qUOLoFd/cV6f4Q7mh7g==
dependencies:
eth-rpc-errors "^3.0.0"
safe-event-emitter "^1.0.1"
json-rpc-random-id@^1.0.0, json-rpc-random-id@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz"
@@ -13160,10 +13229,10 @@ json-stable-stringify@^1.0.1:
dependencies:
jsonify "~0.0.0"
json-stringify-safe@~5.0.1:
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
json-to-pretty-yaml@^1.2.2:
version "1.2.2"
@@ -13211,6 +13280,11 @@ jsonify@~0.0.0:
resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz"
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
jsonparse@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@@ -16733,10 +16807,10 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-transform@^0.14.2:
version "0.14.5"
@@ -17106,7 +17180,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4:
rlp@^2.2.4:
version "2.2.7"
resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf"
integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==
@@ -17153,6 +17227,19 @@ rollup@^1.31.1:
"@types/node" "*"
acorn "^7.1.0"
rpc-websockets@^7.5.1:
version "7.5.1"
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.1.tgz#e0a05d525a97e7efc31a0617f093a13a2e10c401"
integrity sha512-kGFkeTsmd37pHPMaHIgN1LVKXMi0JD782v4Ds9ZKtLlwdTKjn+CxM9A9/gLT2LaOuEcEFGL98h1QWQtlOIdW0w==
dependencies:
"@babel/runtime" "^7.17.2"
eventemitter3 "^4.0.7"
uuid "^8.3.2"
ws "^8.5.0"
optionalDependencies:
bufferutil "^4.0.1"
utf-8-validate "^5.0.2"
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -17518,7 +17605,7 @@ setprototypeof@1.1.1:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
sha.js@^2.4.0, sha.js@^2.4.8:
sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8:
version "2.4.11"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
@@ -18213,6 +18300,11 @@ stylehacks@^4.0.0:
postcss "^7.0.0"
postcss-selector-parser "^3.0.0"
superstruct@^0.14.2:
version "0.14.2"
resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b"
integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -18431,6 +18523,11 @@ test-value@^2.1.0:
array-back "^1.0.3"
typical "^2.6.0"
text-encoding-utf-8@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
text-table@0.2.0, text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
@@ -18459,10 +18556,10 @@ through2@^2.0.0:
readable-stream "~2.3.6"
xtend "~4.0.1"
through@^2.3.6, through@^2.3.8:
"through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8:
version "2.3.8"
resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
thunky@^1.0.2:
version "1.1.0"
@@ -19164,10 +19261,10 @@ use@^3.1.0:
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
utf-8-validate@^5.0.8:
version "5.0.9"
resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.9.tgz#ba16a822fbeedff1a58918f2a6a6b36387493ea3"
integrity sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==
utf-8-validate@^5.0.2, utf-8-validate@^5.0.8:
version "5.0.10"
resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2"
integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==
dependencies:
node-gyp-build "^4.3.0"
@@ -19929,11 +20026,16 @@ ws@7.5.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
ws@8.11.0, ws@>=7.4.6:
ws@8.11.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
ws@>=7.4.6, ws@^8.5.0:
version "8.12.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f"
integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==
ws@^6.2.1:
version "6.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"