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>
This commit is contained in:
Charles Bachmeier 2023-02-28 11:47:22 -08:00 committed by GitHub
parent 5979635939
commit 6131e6bfab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 257 additions and 434 deletions

@ -1,16 +1,25 @@
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 { approveCollectionRow, getListingState, getTotalEthValue, verifyStatus } from 'nft/components/bag/profile/utils'
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'
@ -185,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,
}),
@ -212,31 +205,16 @@ export const ListPage = () => {
)
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
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)
@ -247,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)
@ -317,9 +294,7 @@ export const ListPage = () => {
<EthValueWrapper totalEthListingValue={!!totalEthListingValue}>
{totalEthListingValue > 0 ? formatEth(totalEthListingValue) : '-'} ETH
</EthValueWrapper>
{!!totalEthListingValue && !!ethPriceInUSD && (
<UsdValue>{formatUsdPrice(totalEthListingValue * ethPriceInUSD)}</UsdValue>
)}
{!!usdcValue && <UsdValue>{usdcAmount}</UsdValue>}
</ProceedsWrapper>
<ListingButton onClick={showModalAndStartListing} />
</ProceedsAndButtonWrapper>

@ -2,15 +2,13 @@ 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, useNFTList, useSellAsset } from 'nft/hooks'
import { Listing, ListingStatus, WalletAsset } from 'nft/types'
import { useEffect, useMemo, useState } from 'react'
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'
import { getListings } from '../../bag/profile/utils'
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
const StyledListingButton = styled(BaseButton)<{ showResolveIssues: boolean; missingPrices: boolean }>`
@ -44,25 +42,9 @@ export const ListingButton = ({ onClick }: { onClick: () => void }) => {
}),
shallow
)
const { setListingStatus, setListings, setCollectionsRequiringApproval } = useNFTList(
({ setListingStatus, setListings, setCollectionsRequiringApproval }) => ({
setListingStatus,
setListings,
setCollectionsRequiringApproval,
}),
shallow
)
const [showWarning, setShowWarning] = useState(false)
const isMobile = useIsMobile()
// instantiate listings and collections to approve when user's modify input data
useEffect(() => {
const [newCollectionsToApprove, newListings] = getListings(sellAssets)
setListings(newListings)
setCollectionsRequiringApproval(newCollectionsToApprove)
setListingStatus(ListingStatus.DEFINED)
}, [sellAssets, setCollectionsRequiringApproval, setListingStatus, setListings])
// Find issues with item listing data
const [listingsMissingPrice, listingsBelowFloor] = useMemo(() => {
const missingExpiration = sellAssets.some((asset) => {

@ -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,7 +122,6 @@ export const MarketplaceRow = ({
selectedMarkets,
removeMarket = undefined,
asset,
showMarketplaceLogo,
expandMarketplaceRows,
toggleExpandMarketplaceRows,
rowHovered,
@ -147,9 +137,16 @@ export const MarketplaceRow = ({
)?.price
)
const [globalOverride, setGlobalOverride] = useState(false)
const showGlobalPrice = globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride && globalPrice
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) {
@ -168,68 +165,25 @@ 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 = listPrice
}
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>
@ -263,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 />}

@ -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 { useCallback, 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()
@ -113,8 +112,8 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
}, [allCollectionsApproved])
const closeModalOnClick = useCallback(() => {
listingStatus === ListingStatus.APPROVED ? window.location.reload() : overlayClick()
}, [listingStatus, overlayClick])
allListingsApproved ? window.location.reload() : overlayClick()
}, [allListingsApproved, overlayClick])
// In the case that a user removes all listings via retry logic, close modal
useEffect(() => {
@ -125,7 +124,7 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
<Portal>
<Trace modal={InterfaceModalName.NFT_LISTING}>
<ListModalWrapper>
{listingStatus === ListingStatus.APPROVED ? (
{allListingsApproved ? (
<SuccessScreen overlayClick={closeModalOnClick} />
) : (
<>

@ -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'

@ -10,7 +10,7 @@ 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
@ -123,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
@ -161,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}
@ -186,7 +185,6 @@ export const NFTListRow = ({
setGlobalPrice={setGlobalPrice}
selectedMarkets={localMarkets}
asset={asset}
showMarketplaceLogo={false}
rowHovered={hovered}
toggleExpandMarketplaceRows={toggleExpandMarketplaceRows}
/>

@ -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;
@ -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)

@ -3,16 +3,19 @@ import Column from 'components/Column'
import Row from 'components/Row'
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 { 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;
@ -71,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) {
@ -96,7 +93,6 @@ interface PriceTextInputProps {
isGlobalPrice: boolean
setGlobalOverride: Dispatch<boolean>
globalOverride: boolean
warning?: ListingWarning
asset: WalletAsset
}
@ -106,42 +102,35 @@ export const PriceTextInput = ({
isGlobalPrice,
setGlobalOverride,
globalOverride,
warning,
asset,
}: PriceTextInputProps) => {
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
? (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>
@ -156,13 +145,7 @@ export const PriceTextInput = ({
backgroundColor="none"
width={{ sm: '54', md: '68' }}
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) && (
@ -172,27 +155,25 @@ export const PriceTextInput = ({
)}
</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>
)

@ -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) => (

@ -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,
}

@ -1,9 +1,13 @@
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,
@ -12,10 +16,9 @@ export async function approveCollectionRow(
collection: CollectionRow,
status: ListingStatus,
callback?: () => Promise<void>
) => void,
pauseAllRows?: () => void
) => void
) {
const callback = () => approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows)
const callback = () => approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback)
setCollectionStatusAndCallback(collectionRow, ListingStatus.SIGNING, callback)
const { marketplace, collectionAddress } = collectionRow
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
@ -31,11 +34,6 @@ export async function approveCollectionRow(
(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(
@ -44,31 +42,18 @@ export async function signListingRow(
provider: Web3Provider,
getLooksRareNonce: () => number,
setLooksRareNonce: (nonce: number) => void,
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void,
pauseAllRows?: () => void
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void
) {
const looksRareNonce = getLooksRareNonce()
const callback = () => {
return signListingRow(
listing,
signer,
provider,
getLooksRareNonce,
setLooksRareNonce,
setListingStatusAndCallback,
pauseAllRows
)
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)
)
if (listing.status === ListingStatus.REJECTED && pauseAllRows) {
pauseAllRows()
} else {
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
}
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
}
export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
@ -86,7 +71,7 @@ export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
return total ? Math.round(total * 10000 + Number.EPSILON) / 10000 : 0
}
export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]] => {
const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]] => {
const newCollectionsToApprove: CollectionRow[] = []
const newListings: ListingRow[] = []
@ -123,69 +108,89 @@ export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], Listin
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 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
}

@ -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) => {

@ -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 }

@ -7,8 +7,8 @@ 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'
@ -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)

@ -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 {