import { faCheckCircle } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useConnectModal } from '@rainbow-me/rainbowkit' import { Currency, Listing, useReservoirClient, } from '@reservoir0x/reservoir-kit-ui' import { Execute } from '@reservoir0x/reservoir-sdk' import LoadingSpinner from 'components/common/LoadingSpinner' import { Modal } from 'components/common/Modal' import TransactionProgress from 'components/common/TransactionProgress' import { BatchListing, Marketplace } from 'components/portfolio/BatchListings' import { Box, Button, Flex, Text } from 'components/primitives' import ErrorWell from 'components/primitives/ErrorWell' import dayjs from 'dayjs' import { useMarketplaceChain } from 'hooks' import { UserToken } from 'pages/portfolio/[[...address]]' import { FC, useCallback, useEffect, useState } from 'react' import { useNetwork, useWalletClient, useSwitchNetwork } from 'wagmi' import { ApprovalCollapsible } from './ApprovalCollapsible' import { formatUnits, parseUnits, zeroAddress } from 'viem' import useOnChainRoyalties, { OnChainRoyaltyReturnType, } from 'hooks/useOnChainRoyalties' enum BatchListStep { Approving, Complete, } export type BatchListingData = { listing: Listing token: UserToken } type BatchListModalStepData = { totalSteps: number stepProgress: number currentStep: Execute['steps'][0] listings: BatchListingData[] } type Props = { listings: BatchListing[] disabled: boolean currency: Currency selectedMarketplaces: Marketplace[] onChainRoyalties: ReturnType['data'] onCloseComplete?: () => void } const orderFee = process.env.NEXT_PUBLIC_MARKETPLACE_FEE const orderFees = orderFee ? [orderFee] : [] const BatchListModal: FC = ({ listings, disabled, currency, selectedMarketplaces, onChainRoyalties, onCloseComplete, }) => { const [open, setOpen] = useState(false) const { data: wallet } = useWalletClient() const { openConnectModal } = useConnectModal() const { chain: activeChain } = useNetwork() const marketplaceChain = useMarketplaceChain() const { switchNetworkAsync } = useSwitchNetwork({ chainId: marketplaceChain.id, }) const isInTheWrongNetwork = Boolean( wallet && activeChain?.id !== marketplaceChain.id ) const client = useReservoirClient() const [batchListStep, setBatchListStep] = useState( BatchListStep.Approving ) const [stepData, setStepData] = useState(null) const [transactionError, setTransactionError] = useState() const [stepTitle, setStepTitle] = useState('') const [uniqueMarketplaces, setUniqueMarketplaces] = useState( [] ) const getUniqueMarketplaces = useCallback( (listings: BatchListModalStepData['listings']): Marketplace[] => { const marketplaces: Marketplace[] = [] listings.forEach((listing) => { const marketplace = selectedMarketplaces.find( (m) => m.orderbook === listing.listing.orderbook ) if (marketplace && !marketplaces.includes(marketplace)) { marketplaces.push(marketplace) } }) return marketplaces }, [listings] ) useEffect(() => { if (stepData) { const orderKind = stepData.listings[0].listing.orderKind || 'exchange' const marketplaces = getUniqueMarketplaces(stepData.listings) const marketplaceNames = marketplaces .map((marketplace) => marketplace.name) .join(' and ') setUniqueMarketplaces(marketplaces) switch (stepData.currentStep.kind) { case 'transaction': { setStepTitle( `Approve ${ orderKind?.[0].toUpperCase() + orderKind?.slice(1) } to access item\nin your wallet` ) break } case 'signature': { setStepTitle( `Confirm listings on ${marketplaceNames}\nin your wallet` ) break } } } }, [stepData]) const listTokens = useCallback(() => { if (!wallet) { const error = new Error('Missing a wallet') setTransactionError(error) throw error } if (!client) { const error = new Error('ReservoirClient was not initialized') setTransactionError(error) throw error } setTransactionError(null) const batchListingData: BatchListingData[] = [] listings.forEach((listing, i) => { let expirationTime: string | null = null if ( listing.expirationOption && listing.expirationOption.relativeTime && listing.expirationOption.relativeTimeUnit ) { expirationTime = dayjs() .add( listing.expirationOption.relativeTime, listing.expirationOption.relativeTimeUnit ) .unix() .toString() } const token = `${listing.token.token?.contract}:${listing.token.token?.tokenId}` const convertedListing: Listing = { token: token, weiPrice: ( parseUnits(`${+listing.price}`, currency.decimals || 18) * BigInt(listing.quantity) ).toString(), orderbook: listing.orderbook, orderKind: listing.orderKind, quantity: listing.quantity, } if (listing.orderbook === 'reservoir') { convertedListing.fees = orderFees } if (expirationTime) { convertedListing.expirationTime = expirationTime } if (currency && currency.contract != zeroAddress) { convertedListing.currency = currency.contract } const onChainRoyalty = onChainRoyalties && onChainRoyalties[i] ? onChainRoyalties[i] : null if (onChainRoyalty && listing.orderKind?.includes('seaport')) { convertedListing.automatedRoyalties = false const royaltyData = onChainRoyalty.result as OnChainRoyaltyReturnType const royalties = royaltyData[0].map((recipient, i) => { const bps = Math.floor( (parseFloat( formatUnits( royaltyData[1][i], marketplaceChain?.nativeCurrency.decimals || 18 ) ) / 1) * 10000 ) return `${recipient}:${bps}` }) if (royalties.length > 0) { convertedListing.fees = [...royalties] } } batchListingData.push({ listing: convertedListing, token: listing.token, }) }) setBatchListStep(BatchListStep.Approving) client.actions .listToken({ listings: batchListingData.map((data) => data.listing), wallet, onProgress: (steps: Execute['steps']) => { const executableSteps = steps.filter( (step) => step.items && step.items.length > 0 ) let stepCount = executableSteps.length let incompleteStepItemIndex: number | null = null let incompleteStepIndex: number | null = null executableSteps.find((step, i) => { if (!step.items) { return false } incompleteStepItemIndex = step.items.findIndex( (item) => item.status == 'incomplete' ) if (incompleteStepItemIndex !== -1) { incompleteStepIndex = i return true } }) if ( incompleteStepIndex === null || incompleteStepItemIndex === null ) { const currentStep = executableSteps[executableSteps.length - 1] setBatchListStep(BatchListStep.Complete) setStepData({ totalSteps: stepCount, stepProgress: stepCount, currentStep, listings: batchListingData, }) } else { setStepData({ totalSteps: stepCount, stepProgress: incompleteStepIndex, currentStep: executableSteps[incompleteStepIndex], listings: batchListingData, }) } }, }) .catch((e: any) => { const error = e as Error const transactionError = new Error( 'Oops, something went wrong. Please try again.', { cause: error, } ) setTransactionError(transactionError) }) }, [client, listings, wallet, onChainRoyalties]) const trigger = ( ) if (isInTheWrongNetwork) { return ( ) } return ( { if ( !open && onCloseComplete && batchListStep === BatchListStep.Complete ) { onCloseComplete() } setOpen(open) }} > {batchListStep === BatchListStep.Approving && ( {transactionError && ( )} {!stepData && !transactionError && ( )} {stepData && ( {stepData.currentStep.kind === 'signature' ? ( {stepTitle} ) : null} {stepData.currentStep.kind === 'transaction' ? stepData.currentStep.items?.map((item, i) => { if (item.data) return ( ) }) : null} {stepData.currentStep.kind === 'signature' ? ( listing.token.token?.imageSmall as string )} toImgs={uniqueMarketplaces.map((marketplace) => { return marketplace?.imageUrl || '' })} /> ) : null} )} {stepData?.currentStep.description} {!transactionError ? ( ) : ( )} )} {batchListStep === BatchListStep.Complete && ( Your items have been listed! )} ) } export default BatchListModal