feat: add listing modal (#4663)
* add listing modal * add new files * remove useeffect * re-add useeffect and includes array * position relative * add listing datatype * use pluralize * readable const * clsx * parseFloat 0 default * don't use any * cant use months for ms Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
parent
3d4b077b89
commit
ee97d8d902
253
src/nft/components/sell/modal/ListingButton.tsx
Normal file
253
src/nft/components/sell/modal/ListingButton.tsx
Normal file
@ -0,0 +1,253 @@
|
||||
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 { bodySmall } from 'nft/css/common.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 * as styles from './ListingModal.css'
|
||||
import { getListings } from './utils'
|
||||
|
||||
interface ListingButtonProps {
|
||||
onClick: () => void
|
||||
buttonText: string
|
||||
showWarningOverride?: boolean
|
||||
}
|
||||
|
||||
export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => {
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const addMarketplaceWarning = useSellAsset((state) => state.addMarketplaceWarning)
|
||||
const removeAllMarketplaceWarnings = useSellAsset((state) => state.removeAllMarketplaceWarnings)
|
||||
const listingStatus = useNFTList((state) => state.listingStatus)
|
||||
const setListingStatus = useNFTList((state) => state.setListingStatus)
|
||||
const setListings = useNFTList((state) => state.setListings)
|
||||
const setCollectionsRequiringApproval = useNFTList((state) => state.setCollectionsRequiringApproval)
|
||||
const [showWarning, setShowWarning] = useState(false)
|
||||
const [canContinue, setCanContinue] = useState(false)
|
||||
const warningRef = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(warningRef, () => {
|
||||
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 || asset.marketplaces.length === 0
|
||||
)
|
||||
const missingExpiration = sellAssets.some((asset) => {
|
||||
return asset.expirationTime != null && asset.expirationTime - 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(parseFloat(listing.price)) || parseFloat(listing.price) < 0)
|
||||
invalidPrices.push([asset, listing])
|
||||
else if (parseFloat(listing.price) < asset.floorPrice && !listing.overrideFloorPrice)
|
||||
listingsBelowFloor.push([asset, listing])
|
||||
else if (asset.floor_sell_order_price && parseFloat(listing.price) > asset.floor_sell_order_price)
|
||||
listingsAboveSellOrderFloor.push([asset, listing])
|
||||
}
|
||||
}
|
||||
}
|
||||
const continueCheck = listingsBelowFloor.length === 0 && listingsAboveSellOrderFloor.length === 0
|
||||
setCanContinue(continueCheck)
|
||||
return [
|
||||
noMarketplacesSelected,
|
||||
missingExpiration,
|
||||
invalidExpiration,
|
||||
overMaxExpiration,
|
||||
listingsMissingPrice,
|
||||
listingsBelowFloor,
|
||||
listingsAboveSellOrderFloor,
|
||||
invalidPrices,
|
||||
]
|
||||
}, [sellAssets])
|
||||
|
||||
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) onClick()
|
||||
else addWarningMessages()
|
||||
}
|
||||
|
||||
return (
|
||||
<Box position="relative" width="full">
|
||||
{!showWarningOverride && showWarning && warningMessage.length > 0 && (
|
||||
<Row
|
||||
className={`${bodySmall} ${styles.warningTooltip}`}
|
||||
transition="250"
|
||||
onClick={() => setShowWarning(false)}
|
||||
color="darkGray"
|
||||
zIndex="3"
|
||||
borderRadius="4"
|
||||
backgroundColor="white"
|
||||
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="darkGray" height="20" width="20" />
|
||||
</Box>
|
||||
) : (
|
||||
<Row
|
||||
marginLeft="72"
|
||||
cursor="pointer"
|
||||
color="genieBlue"
|
||||
onClick={() => {
|
||||
setShowWarning(false)
|
||||
setCanContinue(true)
|
||||
onClick()
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
<ArrowRightIcon height="20" width="20" />
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
<Box
|
||||
as="button"
|
||||
border="none"
|
||||
backgroundColor="genieBlue"
|
||||
cursor={
|
||||
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
|
||||
disableListButton
|
||||
? 'default'
|
||||
: 'pointer'
|
||||
}
|
||||
color="explicitWhite"
|
||||
className={styles.button}
|
||||
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
|
||||
type="button"
|
||||
style={{
|
||||
opacity:
|
||||
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
|
||||
disableListButton
|
||||
? 0.3
|
||||
: 1,
|
||||
}}
|
||||
>
|
||||
{listingStatus === ListingStatus.SIGNING || listingStatus === ListingStatus.PENDING ? (
|
||||
<Row gap="8">
|
||||
<LoadingIcon stroke="white" 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'
|
||||
) : (
|
||||
buttonText
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
79
src/nft/components/sell/modal/ListingModal.css.ts
Normal file
79
src/nft/components/sell/modal/ListingModal.css.ts
Normal file
@ -0,0 +1,79 @@
|
||||
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)',
|
||||
})
|
||||
|
||||
export const sectionDivider = style([
|
||||
sprinkles({
|
||||
borderRadius: '20',
|
||||
marginTop: '8',
|
||||
width: 'full',
|
||||
borderWidth: '0.5px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'medGray',
|
||||
}),
|
||||
])
|
||||
|
||||
export const button = style([
|
||||
sprinkles({
|
||||
height: '40',
|
||||
width: 'full',
|
||||
textAlign: 'center',
|
||||
fontWeight: 'medium',
|
||||
fontSize: '14',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: '12',
|
||||
}),
|
||||
{
|
||||
lineHeight: '18px',
|
||||
},
|
||||
])
|
||||
|
||||
export const listingModalIcon = style([
|
||||
sprinkles({
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'white',
|
||||
}),
|
||||
{
|
||||
boxSizing: 'border-box',
|
||||
marginLeft: '-2px',
|
||||
},
|
||||
])
|
||||
|
||||
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: 'medGray',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
}),
|
||||
])
|
237
src/nft/components/sell/modal/ListingModal.tsx
Normal file
237
src/nft/components/sell/modal/ListingModal.tsx
Normal file
@ -0,0 +1,237 @@
|
||||
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
|
||||
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 { pluralize } from 'nft/utils/roundAndPluralize'
|
||||
import { Dispatch, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
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 signer = provider?.getSigner()
|
||||
const listings = useNFTList((state) => state.listings)
|
||||
const setListings = useNFTList((state) => state.setListings)
|
||||
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
|
||||
const setCollectionsRequiringApproval = useNFTList((state) => state.setCollectionsRequiringApproval)
|
||||
const [openIndex, setOpenIndex] = useState(0)
|
||||
const listingStatus = useNFTList((state) => state.listingStatus)
|
||||
const setListingStatus = useNFTList((state) => state.setListingStatus)
|
||||
const [allCollectionsApproved, setAllCollectionsApproved] = useState(false)
|
||||
const looksRareNonce = useNFTList((state) => state.looksRareNonce)
|
||||
const setLooksRareNonce = useNFTList((state) => state.setLooksRareNonce)
|
||||
const getLooksRareNonce = useNFTList((state) => state.getLooksRareNonce)
|
||||
const toggleCart = useBag((state) => state.toggleBag)
|
||||
const looksRareNonceRef = useRef(looksRareNonce)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
useEffect(() => {
|
||||
useNFTList.subscribe((state) => (looksRareNonceRef.current = state.looksRareNonce))
|
||||
}, [])
|
||||
|
||||
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
|
||||
|
||||
// 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)
|
||||
) {
|
||||
signListings()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [collectionsRequiringApproval, allCollectionsApproved])
|
||||
|
||||
// handles the modal wide listing state based on conglomeration of the wallet, collection, and listing states
|
||||
|
||||
const startListingFlow = async () => {
|
||||
if (!signer) return
|
||||
setListingStatus(ListingStatus.SIGNING)
|
||||
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
|
||||
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)
|
||||
}
|
||||
const looksRareAddress = addresses.TRANSFER_MANAGER_ERC721
|
||||
// for all unqiue collection, marketplace combos -> approve collections
|
||||
for (const collectionRow of collectionsRequiringApproval) {
|
||||
verifyStatus(collectionRow.status) &&
|
||||
approveCollectionRow(
|
||||
collectionRow,
|
||||
collectionsRequiringApproval,
|
||||
setCollectionsRequiringApproval,
|
||||
signer,
|
||||
looksRareAddress,
|
||||
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,
|
||||
listings,
|
||||
setListings,
|
||||
signer,
|
||||
provider,
|
||||
getLooksRareNonce,
|
||||
setLooksRareNonce,
|
||||
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)
|
||||
}
|
||||
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 (
|
||||
<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="darkGray"
|
||||
backgroundColor="white"
|
||||
marginLeft="auto"
|
||||
marginRight="0"
|
||||
paddingRight="0"
|
||||
display={{ sm: 'flex', md: 'none' }}
|
||||
cursor="pointer"
|
||||
onClick={toggleCart}
|
||||
>
|
||||
<XMarkIcon height={28} width={28} fill={themeVars.colors.blackBlue} />
|
||||
</Box>
|
||||
</Row>
|
||||
<Column overflowX="hidden" overflowY="auto" style={{ maxHeight: '60vh' }}>
|
||||
{showSuccessScreen ? (
|
||||
<ListingSection
|
||||
sectionTitle={`Listed ${listings.length} item${pluralize(listings.length)} for sale`}
|
||||
rows={listings}
|
||||
index={0}
|
||||
openIndex={openIndex}
|
||||
isSuccessScreen={true}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<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}
|
||||
ETH
|
||||
</Row>
|
||||
</Row>
|
||||
{showSuccessScreen ? (
|
||||
<Box as="span" className={caption} color="darkGray">
|
||||
Status:{' '}
|
||||
<Box as="span" color="green200">
|
||||
Confirmed
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<ListingButton onClick={clickStartListingFlow} buttonText={'Start listing'} showWarningOverride={isMobile} />
|
||||
)}
|
||||
{(listingStatus === ListingStatus.PENDING || listingStatus === ListingStatus.SIGNING) && (
|
||||
<Box
|
||||
as="button"
|
||||
border="none"
|
||||
backgroundColor="white"
|
||||
cursor="pointer"
|
||||
color="orange"
|
||||
className={styles.button}
|
||||
onClick={clickStopListing}
|
||||
type="button"
|
||||
>
|
||||
Stop listing
|
||||
</Box>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListingModal
|
188
src/nft/components/sell/modal/ListingSection.tsx
Normal file
188
src/nft/components/sell/modal/ListingSection.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
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 parseFloat(
|
||||
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 ? 'green' : 'blackBlue'}
|
||||
>
|
||||
{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="blackBlue" fontWeight="normal" className={caption}>
|
||||
{caption}
|
||||
</Box>
|
||||
)}
|
||||
{title && (
|
||||
<Box color="darkGray" 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="blackBlue"
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
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="darkGray">
|
||||
{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="genieBlue"
|
||||
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>
|
||||
)
|
||||
}
|
@ -11,6 +11,7 @@ import { useLocation } from 'react-router-dom'
|
||||
import * as styles from './ListingTag.css'
|
||||
|
||||
const CartSellAssetRow = lazy(() => import('./TagAssetRow'))
|
||||
const ListingModal = lazy(() => import('./ListingModal'))
|
||||
|
||||
const Cart = () => {
|
||||
const { pathname } = useLocation()
|
||||
@ -44,7 +45,9 @@ const Cart = () => {
|
||||
marginLeft="0"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
{sellPageState === SellPageStateType.LISTING ? null : (
|
||||
{sellPageState === SellPageStateType.LISTING ? (
|
||||
<ListingModal />
|
||||
) : (
|
||||
<>
|
||||
<BagHeader bagQuantity={sellAssets.length} />
|
||||
<Column
|
||||
|
305
src/nft/components/sell/modal/utils.ts
Normal file
305
src/nft/components/sell/modal/utils.ts
Normal file
@ -0,0 +1,305 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import type { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
|
||||
import { ConsiderationInputItem } from '@opensea/seaport-js/lib/types'
|
||||
import {
|
||||
INVERSE_BASIS_POINTS,
|
||||
OPENSEA_CROSS_CHAIN_CONDUIT,
|
||||
OPENSEA_DEFAULT_FEE,
|
||||
OPENSEA_FEE_ADDRESS,
|
||||
} from 'nft/queries/openSea'
|
||||
import { AssetRow, CollectionRow, ListingMarket, ListingRow, ListingStatus, WalletAsset } from 'nft/types'
|
||||
import { approveCollection, signListing } from 'nft/utils/listNfts'
|
||||
import { Dispatch } from 'react'
|
||||
|
||||
export 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,
|
||||
collectionsRequiringApproval: CollectionRow[],
|
||||
setCollectionsRequiringApproval: Dispatch<CollectionRow[]>,
|
||||
signer: JsonRpcSigner,
|
||||
looksRareAddress: string,
|
||||
pauseAllRows: () => void
|
||||
) {
|
||||
updateStatus({
|
||||
listing: collectionRow,
|
||||
newStatus: ListingStatus.SIGNING,
|
||||
rows: collectionsRequiringApproval,
|
||||
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
|
||||
callback: () =>
|
||||
approveCollectionRow(
|
||||
collectionRow,
|
||||
collectionsRequiringApproval,
|
||||
setCollectionsRequiringApproval,
|
||||
signer,
|
||||
looksRareAddress,
|
||||
pauseAllRows
|
||||
),
|
||||
})
|
||||
const { marketplace, collectionAddress } = collectionRow
|
||||
const spender =
|
||||
marketplace.name === 'OpenSea'
|
||||
? OPENSEA_CROSS_CHAIN_CONDUIT
|
||||
: marketplace.name === 'Rarible'
|
||||
? process.env.REACT_APP_LOOKSRARE_MARKETPLACE_CONTRACT
|
||||
: marketplace.name === 'X2Y2'
|
||||
? process.env.REACT_APP_X2Y2_TRANSFER_CONTRACT
|
||||
: looksRareAddress
|
||||
await approveCollection(spender ?? '', collectionAddress, signer, (newStatus: ListingStatus) =>
|
||||
updateStatus({
|
||||
listing: collectionRow,
|
||||
newStatus,
|
||||
rows: collectionsRequiringApproval,
|
||||
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
|
||||
})
|
||||
)
|
||||
if (collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) pauseAllRows()
|
||||
}
|
||||
|
||||
export async function signListingRow(
|
||||
listing: ListingRow,
|
||||
listings: ListingRow[],
|
||||
setListings: Dispatch<ListingRow[]>,
|
||||
signer: JsonRpcSigner,
|
||||
provider: Web3Provider,
|
||||
getLooksRareNonce: () => number,
|
||||
setLooksRareNonce: (nonce: number) => void,
|
||||
pauseAllRows: () => void
|
||||
) {
|
||||
const looksRareNonce = getLooksRareNonce()
|
||||
updateStatus({
|
||||
listing,
|
||||
newStatus: ListingStatus.SIGNING,
|
||||
rows: listings,
|
||||
setRows: setListings as Dispatch<AssetRow[]>,
|
||||
callback: () => {
|
||||
return signListingRow(
|
||||
listing,
|
||||
listings,
|
||||
setListings,
|
||||
signer,
|
||||
provider,
|
||||
getLooksRareNonce,
|
||||
setLooksRareNonce,
|
||||
pauseAllRows
|
||||
)
|
||||
},
|
||||
})
|
||||
const { asset, marketplace } = listing
|
||||
const res = await signListing(marketplace, asset, signer, provider, looksRareNonce, (newStatus: ListingStatus) =>
|
||||
updateStatus({
|
||||
listing,
|
||||
newStatus,
|
||||
rows: listings,
|
||||
setRows: setListings as Dispatch<AssetRow[]>,
|
||||
})
|
||||
)
|
||||
if (listing.status === ListingStatus.REJECTED) pauseAllRows()
|
||||
else {
|
||||
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
|
||||
const newStatus = res ? ListingStatus.APPROVED : ListingStatus.FAILED
|
||||
updateStatus({
|
||||
listing,
|
||||
newStatus,
|
||||
rows: listings,
|
||||
setRows: setListings as Dispatch<AssetRow[]>,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
|
||||
const total = sellAssets.reduce((total, asset: WalletAsset) => {
|
||||
if (asset.newListings?.length) {
|
||||
const maxListing = asset.newListings.reduce((a, b) =>
|
||||
parseFloat(a.price ?? '0') > parseFloat(b.price ?? '0') ? a : b
|
||||
)
|
||||
return (
|
||||
total +
|
||||
parseFloat(maxListing.price ?? '0') -
|
||||
parseFloat(maxListing.price ?? '0') * (maxListing.marketplace.fee / 100 + asset.creatorPercentage)
|
||||
)
|
||||
}
|
||||
return total
|
||||
}, 0)
|
||||
return total ? Math.round(total * 100 + Number.EPSILON) / 100 : 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.image_preview_url, marketplace.icon],
|
||||
name: asset.name || `#${asset.tokenId}`,
|
||||
status: ListingStatus.DEFINED,
|
||||
asset,
|
||||
marketplace,
|
||||
}
|
||||
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,
|
||||
marketplace,
|
||||
}
|
||||
newCollectionsToApprove.push(newCollectionRow)
|
||||
}
|
||||
})
|
||||
})
|
||||
return [newCollectionsToApprove, newListings]
|
||||
}
|
||||
|
||||
export 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,
|
||||
})
|
||||
}
|
||||
|
||||
const createConsiderationItem = (basisPoints: string, recipient: string): ConsiderationInputItem => {
|
||||
return {
|
||||
amount: basisPoints,
|
||||
recipient,
|
||||
}
|
||||
}
|
||||
|
||||
export const getConsiderationItems = (
|
||||
asset: WalletAsset,
|
||||
price: BigNumber,
|
||||
signerAddress: string
|
||||
): {
|
||||
sellerFee: ConsiderationInputItem
|
||||
openseaFee: ConsiderationInputItem
|
||||
creatorFee?: ConsiderationInputItem
|
||||
} => {
|
||||
const openSeaBasisPoints = OPENSEA_DEFAULT_FEE * INVERSE_BASIS_POINTS
|
||||
const creatorFeeBasisPoints = asset.creatorPercentage * INVERSE_BASIS_POINTS
|
||||
const sellerBasisPoints = INVERSE_BASIS_POINTS - openSeaBasisPoints - creatorFeeBasisPoints
|
||||
|
||||
const openseaFee = price.mul(BigNumber.from(openSeaBasisPoints)).div(BigNumber.from(INVERSE_BASIS_POINTS)).toString()
|
||||
const creatorFee = price
|
||||
.mul(BigNumber.from(creatorFeeBasisPoints))
|
||||
.div(BigNumber.from(INVERSE_BASIS_POINTS))
|
||||
.toString()
|
||||
const sellerFee = price.mul(BigNumber.from(sellerBasisPoints)).div(BigNumber.from(INVERSE_BASIS_POINTS)).toString()
|
||||
|
||||
return {
|
||||
sellerFee: createConsiderationItem(sellerFee, signerAddress),
|
||||
openseaFee: createConsiderationItem(openseaFee, OPENSEA_FEE_ADDRESS),
|
||||
creatorFee:
|
||||
creatorFeeBasisPoints > 0 ? createConsiderationItem(creatorFee, asset.asset_contract.payout_address) : undefined,
|
||||
}
|
||||
}
|
@ -249,7 +249,7 @@ const flexAlignment = [
|
||||
|
||||
const overflow = ['hidden', 'inherit', 'scroll', 'visible', 'auto'] as const
|
||||
|
||||
const borderWidth = ['0px', '1px', '1.5px', '2px', '4px']
|
||||
const borderWidth = ['0px', '0.5px', '1px', '1.5px', '2px', '4px']
|
||||
|
||||
const borderStyle = ['none', 'solid'] as const
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
export * from './useBag'
|
||||
export * from './useCollectionFilters'
|
||||
export * from './useFiltersExpanded'
|
||||
export * from './useGenieList'
|
||||
export * from './useIsMobile'
|
||||
export * from './useIsTablet'
|
||||
export * from './useMarketplaceSelect'
|
||||
export * from './useNFTList'
|
||||
export * from './useNFTSelect'
|
||||
export * from './useSearchHistory'
|
||||
export * from './useSelectAsset'
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { AssetRow, ListingMarket } from '../../types'
|
||||
import { ListingMarket, ListingRow } from 'nft/types'
|
||||
|
||||
interface Listing extends AssetRow {
|
||||
interface Listing extends ListingRow {
|
||||
marketplaces: ListingMarket[]
|
||||
}
|
||||
|
||||
export const logListing = async (listings: AssetRow[], userAddress: string): Promise<boolean> => {
|
||||
export const logListing = async (listings: ListingRow[], userAddress: string): Promise<boolean> => {
|
||||
const url = `${process.env.REACT_APP_GENIE_API_URL}/logGenieList`
|
||||
const listingsConsolidated: Listing[] = listings.map((el) => ({ ...el, marketplaces: [] }))
|
||||
const marketplacesById: Record<string, ListingMarket[]> = {}
|
||||
|
@ -28,6 +28,12 @@ export interface SellOrder {
|
||||
tokenReserves?: number
|
||||
}
|
||||
|
||||
export interface Listing {
|
||||
price?: string
|
||||
marketplace: ListingMarket
|
||||
overrideFloorPrice?: boolean
|
||||
}
|
||||
|
||||
export interface WalletAsset {
|
||||
id?: string
|
||||
image_url: string
|
||||
@ -60,11 +66,7 @@ export interface WalletAsset {
|
||||
// Used for creating new listings
|
||||
expirationTime?: number
|
||||
marketAgnosticPrice?: string
|
||||
newListings?: {
|
||||
price?: string
|
||||
marketplace: ListingMarket
|
||||
overrideFloorPrice?: boolean
|
||||
}[]
|
||||
newListings?: Listing[]
|
||||
marketplaces?: ListingMarket[]
|
||||
listingWarnings?: ListingWarning[]
|
||||
}
|
||||
@ -88,19 +90,19 @@ export enum ListingStatus {
|
||||
SIGNING = 'Signing',
|
||||
}
|
||||
|
||||
export interface ListingRow {
|
||||
export interface AssetRow {
|
||||
images: string[]
|
||||
name: string
|
||||
status: ListingStatus
|
||||
callback?: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface AssetRow extends ListingRow {
|
||||
export interface ListingRow extends AssetRow {
|
||||
asset: WalletAsset
|
||||
marketplace: ListingMarket
|
||||
}
|
||||
|
||||
export interface CollectionRow extends ListingRow {
|
||||
export interface CollectionRow extends AssetRow {
|
||||
collectionAddress: string
|
||||
marketplace: ListingMarket
|
||||
}
|
||||
|
@ -3,3 +3,5 @@ export const roundAndPluralize = (i: number, word: string) => {
|
||||
|
||||
return `${rounded} ${word}${rounded === 1 ? '' : 's'}`
|
||||
}
|
||||
|
||||
export const pluralize = (number: number) => (number !== 1 ? 's' : '')
|
||||
|
Loading…
Reference in New Issue
Block a user