Commit a19aa303 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: list 1155s (#6193)

* undisable 1155s and add nftStandard to CollectionRow

* working OS 1155 listing

* amend OS listing to handle 0 creator fee

* handle no royalties set

* disable already listed protection for 1155s

* can list to LR

* stuck on x2

* working x2y2

* remove comment

* add listing issue finding to helper fn

---------
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent c7f67437
import { Plural, t, Trans } from '@lingui/macro'
import { BaseButton } from 'components/Button'
import ms from 'ms.macro'
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
import { useIsMobile, useSellAsset } from 'nft/hooks'
import { Listing, WalletAsset } from 'nft/types'
import { useMemo, useState } from 'react'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { shallow } from 'zustand/shallow'
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
import { findListingIssues } from './utils'
const StyledListingButton = styled(BaseButton)<{ showResolveIssues: boolean; missingPrices: boolean }>`
background: ${({ showResolveIssues, theme }) => (showResolveIssues ? theme.accentFailure : theme.accentAction)};
......@@ -47,35 +45,13 @@ export const ListingButton = ({ onClick }: { onClick: () => void }) => {
// Find issues with item listing data
const [listingsMissingPrice, listingsBelowFloor] = useMemo(() => {
const missingExpiration = sellAssets.some((asset) => {
return (
asset.expirationTime != null &&
(isNaN(asset.expirationTime) || asset.expirationTime * 1000 - Date.now() < ms`60 seconds`)
)
})
const overMaxExpiration = sellAssets.some((asset) => {
return asset.expirationTime != null && asset.expirationTime * 1000 - Date.now() > ms`180 days`
})
const listingsMissingPrice: [WalletAsset, Listing][] = []
const listingsBelowFloor: [WalletAsset, Listing][] = []
const listingsAboveSellOrderFloor: [WalletAsset, Listing][] = []
const invalidPrices: [WalletAsset, Listing][] = []
for (const asset of sellAssets) {
if (asset.newListings) {
for (const listing of asset.newListings) {
if (!listing.price) listingsMissingPrice.push([asset, listing])
else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
else if (
listing.price < (asset?.floorPrice ?? 0) * BELOW_FLOOR_PRICE_THRESHOLD &&
!listing.overrideFloorPrice
)
listingsBelowFloor.push([asset, listing])
else if (asset.floor_sell_order_price && listing.price >= asset.floor_sell_order_price)
listingsAboveSellOrderFloor.push([asset, listing])
}
}
}
const {
missingExpiration,
overMaxExpiration,
listingsMissingPrice,
listingsBelowFloor,
listingsAboveSellOrderFloor,
} = findListingIssues(sellAssets)
// set number of issues
const foundIssues =
......
......@@ -4,7 +4,12 @@ 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 {
getMarketplaceFee,
getRoyalty,
useHandleGlobalPriceToggle,
useSyncPriceWithGlobalMethod,
} from 'nft/components/profile/list/utils'
import { useSellAsset } from 'nft/hooks'
import { ListingMarket, WalletAsset } from 'nft/types'
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
......@@ -150,11 +155,11 @@ export const MarketplaceRow = ({
const fees = useMemo(() => {
if (selectedMarkets.length === 1) {
return getRoyalty(selectedMarkets[0], asset) + selectedMarkets[0].fee
return getRoyalty(selectedMarkets[0], asset) + getMarketplaceFee(selectedMarkets[0], asset)
} else {
let max = 0
for (const selectedMarket of selectedMarkets) {
const fee = selectedMarket.fee + getRoyalty(selectedMarket, asset)
const fee = getRoyalty(selectedMarket, asset) + getMarketplaceFee(selectedMarket, asset)
max = Math.max(fee, max)
}
......
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import Row from 'components/Row'
import { getRoyalty } from 'nft/components/profile/list/utils'
import { getMarketplaceFee, getRoyalty } from 'nft/components/profile/list/utils'
import { ListingMarket, WalletAsset } from 'nft/types'
import { formatEth } from 'nft/utils'
import styled from 'styled-components/macro'
......@@ -63,7 +63,7 @@ export const RoyaltyTooltip = ({
<Trans>fee</Trans>
</ThemedText.Caption>
</Row>
<FeePercent>{market.fee}%</FeePercent>
<FeePercent>{getMarketplaceFee(market, asset)}%</FeePercent>
</FeeWrap>
))}
<FeeWrap>
......
import type { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import ms from 'ms.macro'
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 {
LOOKSRARE_MARKETPLACE_CONTRACT_721,
LOOKSRARE_MARKETPLACE_CONTRACT_1155,
X2Y2_TRANSFER_CONTRACT_721,
X2Y2_TRANSFER_CONTRACT_1155,
} from 'nft/queries'
import { OPENSEA_CROSS_CHAIN_CONDUIT } from 'nft/queries/openSea'
import { CollectionRow, ListingMarket, ListingRow, ListingStatus, WalletAsset } from 'nft/types'
import { CollectionRow, Listing, 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'
......@@ -20,19 +27,27 @@ export async function approveCollectionRow(
) {
const callback = () => approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback)
setCollectionStatusAndCallback(collectionRow, ListingStatus.SIGNING, callback)
const { marketplace, collectionAddress } = collectionRow
const { marketplace, collectionAddress, nftStandard } = collectionRow
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const spender =
marketplace.name === 'OpenSea'
? OPENSEA_CROSS_CHAIN_CONDUIT
: marketplace.name === 'Rarible'
? LOOKSRARE_MARKETPLACE_CONTRACT
: marketplace.name === 'LooksRare'
? collectionRow.nftStandard === NftStandard.Erc721
? LOOKSRARE_MARKETPLACE_CONTRACT_721
: LOOKSRARE_MARKETPLACE_CONTRACT_1155
: marketplace.name === 'X2Y2'
? X2Y2_TRANSFER_CONTRACT
? collectionRow.nftStandard === NftStandard.Erc721
? X2Y2_TRANSFER_CONTRACT_721
: X2Y2_TRANSFER_CONTRACT_1155
: addresses.TRANSFER_MANAGER_ERC721
!!collectionAddress &&
(await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
setCollectionStatusAndCallback(collectionRow, newStatus, callback)
(await approveCollection(
spender,
collectionAddress,
signer,
(newStatus: ListingStatus) => setCollectionStatusAndCallback(collectionRow, newStatus, callback),
nftStandard
))
}
......@@ -100,6 +115,7 @@ const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]]
collectionAddress: asset.asset_contract.address,
isVerified: asset.collectionIsVerified,
marketplace,
nftStandard: asset.asset_contract.tokenType,
}
newCollectionsToApprove.push(newCollectionRow)
}
......@@ -183,14 +199,75 @@ export function useUpdateInputAndWarnings(
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)
else if (
asset.floor_sell_order_price &&
price >= asset.floor_sell_order_price &&
asset.asset_contract.tokenType !== NftStandard.Erc1155
)
setWarningType(WarningType.ALREADY_LISTED)
}, [asset?.floorPrice, asset.floor_sell_order_price, inputRef, listPrice, setWarningType])
}, [
asset.asset_contract.tokenType,
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
// LooksRare is a unique case where royalties for creators are a flat 0.5% or 50 basis points if royalty is set
const baseFee =
listingMarket.name === 'LooksRare'
? asset.basisPoints
? LOOKS_RARE_CREATOR_BASIS_POINTS
: 0
: asset.basisPoints ?? 0
return baseFee * 0.01
}
// OpenSea has a 0.5% fee for all assets that do not have a royalty set
export const getMarketplaceFee = (listingMarket: ListingMarket, asset: WalletAsset) => {
return listingMarket.name === 'OpenSea' && !asset.basisPoints ? 0.5 : listingMarket.fee
}
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
export const findListingIssues = (sellAssets: WalletAsset[]) => {
const missingExpiration = sellAssets.some((asset) => {
return (
asset.expirationTime != null &&
(isNaN(asset.expirationTime) || asset.expirationTime * 1000 - Date.now() < ms`60 seconds`)
)
})
const overMaxExpiration = sellAssets.some((asset) => {
return asset.expirationTime != null && asset.expirationTime * 1000 - Date.now() > ms`180 days`
})
const listingsMissingPrice: [WalletAsset, Listing][] = []
const listingsBelowFloor: [WalletAsset, Listing][] = []
const listingsAboveSellOrderFloor: [WalletAsset, Listing][] = []
for (const asset of sellAssets) {
if (asset.newListings) {
for (const listing of asset.newListings) {
if (!listing.price) listingsMissingPrice.push([asset, listing])
else if (listing.price < (asset?.floorPrice ?? 0) * BELOW_FLOOR_PRICE_THRESHOLD && !listing.overrideFloorPrice)
listingsBelowFloor.push([asset, listing])
else if (
asset.floor_sell_order_price &&
listing.price >= asset.floor_sell_order_price &&
asset.asset_contract.tokenType !== NftStandard.Erc1155
)
listingsAboveSellOrderFloor.push([asset, listing])
}
}
}
return {
missingExpiration,
overMaxExpiration,
listingsMissingPrice,
listingsBelowFloor,
listingsAboveSellOrderFloor,
}
}
......@@ -2,7 +2,6 @@ import { Trans } from '@lingui/macro'
import { useTrace } from '@uniswap/analytics'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { NftCard, NftCardDisplayProps } from 'nft/components/card'
import { VerifiedIcon } from 'nft/components/icons'
import { useBag, useIsMobile, useSellAsset } from 'nft/hooks'
......@@ -59,7 +58,7 @@ export const ViewMyNftsAsset = ({
toggleCart()
}
const isDisabled = asset.asset_contract.tokenType === NftStandard.Erc1155 || asset.susFlag
const isDisabled = asset.susFlag
const display: NftCardDisplayProps = useMemo(() => {
return {
......
export const LOOKSRARE_MARKETPLACE_CONTRACT = '0x59728544B08AB483533076417FbBB2fD0B17CE3a'
export const LOOKSRARE_MARKETPLACE_CONTRACT_721 = '0x59728544B08AB483533076417FbBB2fD0B17CE3a'
export const LOOKSRARE_MARKETPLACE_CONTRACT_1155 = '0xfed24ec7e22f573c2e08aef55aa6797ca2b3a051'
export const OPENSEA_FEE_ADDRESS = '0x0000a26b00c1F0DF003000390027140000fAa719'
export const OPENSEA_DEFAULT_CROSS_CHAIN_CONDUIT_KEY =
'0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000'
export const OPENSEA_CROSS_CHAIN_CONDUIT = '0x1e0049783f008a0085193e00003d00cd54003c71'
......
import { OrderPayload } from '../../utils/x2y2'
export const X2Y2_TRANSFER_CONTRACT = '0xf849de01b080adc3a814fabe1e2087475cf2e354'
export const X2Y2_TRANSFER_CONTRACT_721 = '0xf849de01b080adc3a814fabe1e2087475cf2e354'
export const X2Y2_TRANSFER_CONTRACT_1155 = '0x024ac22acdb367a3ae52a3d94ac6649fdc1f0779'
export const newX2Y2Order = async (payload: OrderPayload): Promise<boolean> => {
const body = JSON.stringify(payload)
......
......@@ -105,6 +105,7 @@ export interface CollectionRow extends AssetRow {
collectionAddress?: string
isVerified?: boolean
marketplace: ListingMarket
nftStandard?: NftStandard
}
// Creating this as an enum and not boolean as we will likely have a success screen state to show
......
......@@ -8,17 +8,20 @@ import { Seaport } from '@opensea/seaport-js'
import { ItemType } from '@opensea/seaport-js/lib/constants'
import { ConsiderationInputItem } from '@opensea/seaport-js/lib/types'
import { ZERO_ADDRESS } from 'constants/misc'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import {
OPENSEA_DEFAULT_CROSS_CHAIN_CONDUIT_KEY,
OPENSEA_FEE_ADDRESS,
OPENSEA_KEY_TO_CONDUIT,
OPENSEA_SEAPORT_V1_4_CONTRACT,
} from 'nft/queries/openSea'
import ERC721 from '../../abis/erc721.json'
import ERC1155 from '../../abis/erc1155.json'
import {
createLooksRareOrder,
getOrderId,
LOOKSRARE_MARKETPLACE_CONTRACT,
LOOKSRARE_MARKETPLACE_CONTRACT_721,
newX2Y2Order,
PostOpenSeaSellOrder,
} from '../queries'
......@@ -60,15 +63,18 @@ const getConsiderationItems = (
): {
sellerFee: ConsiderationInputItem
creatorFee?: ConsiderationInputItem
openSeaFee?: ConsiderationInputItem
} => {
const creatorFeeBasisPoints = asset?.basisPoints ?? 0
const sellerBasisPoints = INVERSE_BASIS_POINTS - creatorFeeBasisPoints
const openSeaBasisPoints = !asset?.basisPoints ? 50 : 0
const sellerBasisPoints = INVERSE_BASIS_POINTS - creatorFeeBasisPoints - openSeaBasisPoints
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()
const openSeaFee = price.mul(BigNumber.from(openSeaBasisPoints)).div(BigNumber.from(INVERSE_BASIS_POINTS)).toString()
return {
sellerFee: createConsiderationItem(sellerFee, signerAddress),
......@@ -76,6 +82,7 @@ const getConsiderationItems = (
creatorFeeBasisPoints > 0
? createConsiderationItem(creatorFee, asset?.asset_contract?.payout_address ?? '')
: undefined,
openSeaFee: openSeaBasisPoints ? createConsiderationItem(openSeaFee, OPENSEA_FEE_ADDRESS) : undefined,
}
}
......@@ -83,22 +90,21 @@ export async function approveCollection(
operator: string,
collectionAddress: string,
signer: Signer,
setStatus: (newStatus: ListingStatus) => void
setStatus: (newStatus: ListingStatus) => void,
nftStandard: NftStandard = NftStandard.Erc721
): Promise<void> {
// This will work for both 721s & 1155s because they both have the
// setApprovalForAll() method
const ERC721Contract = new Contract(collectionAddress, ERC721, signer)
const contract = new Contract(collectionAddress, nftStandard === NftStandard.Erc721 ? ERC721 : ERC1155, signer)
const signerAddress = await signer.getAddress()
try {
const approved = await ERC721Contract.isApprovedForAll(signerAddress, operator)
const approved = await contract.isApprovedForAll(signerAddress, operator)
if (approved) {
setStatus(ListingStatus.APPROVED)
return
}
setStatus(ListingStatus.SIGNING)
const approvalTransaction = await ERC721Contract.setApprovalForAll(operator, true)
const approvalTransaction = await contract.setApprovalForAll(operator, true)
setStatus(ListingStatus.PENDING)
const tx = await approvalTransaction.wait()
......@@ -133,8 +139,8 @@ export async function signListing(
case 'OpenSea':
try {
const listingInWei = parseEther(`${listingPrice}`)
const { sellerFee, creatorFee } = getConsiderationItems(asset, listingInWei, signerAddress)
const considerationItems = [sellerFee, creatorFee].filter(
const { sellerFee, creatorFee, openSeaFee } = getConsiderationItems(asset, listingInWei, signerAddress)
const considerationItems = [sellerFee, creatorFee, openSeaFee].filter(
(item): item is ConsiderationInputItem => item !== undefined
)
......@@ -142,9 +148,10 @@ export async function signListing(
{
offer: [
{
itemType: ItemType.ERC721,
itemType: asset.asset_contract.tokenType === NftStandard.Erc721 ? ItemType.ERC721 : ItemType.ERC1155,
token: asset.asset_contract.address,
identifier: asset.tokenId,
amount: '1',
},
],
consideration: considerationItems,
......@@ -206,7 +213,7 @@ export async function signListing(
signer,
SupportedChainId.MAINNET,
makerOrder,
LOOKSRARE_MARKETPLACE_CONTRACT
LOOKSRARE_MARKETPLACE_CONTRACT_721
)
setStatus(ListingStatus.PENDING)
const payload = {
......@@ -241,10 +248,11 @@ export async function signListing(
{
token: asset.asset_contract.address,
tokenId: BigNumber.from(asset.tokenId),
amount: 1,
},
],
}
const order = createSellOrder(signerAddress, asset.expirationTime, [orderItem])
const order = createSellOrder(signerAddress, asset.expirationTime, [orderItem], asset.asset_contract.tokenType)
try {
const prevOrderId = await getOrderId(asset.asset_contract.address, asset.tokenId)
await signOrderData(provider, order)
......
......@@ -5,8 +5,9 @@ import { AddressZero } from '@ethersproject/constants'
import { keccak256 } from '@ethersproject/keccak256'
import type { Web3Provider } from '@ethersproject/providers'
import { randomBytes } from '@ethersproject/random'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
const dataParamType = `tuple(address token, uint256 tokenId)[]`
const dataParamType = `tuple(address token, uint256 tokenId, uint256 amount)[]`
const orderItemParamType = `tuple(uint256 price, bytes data)`
const orderParamTypes = [
`uint256`,
......@@ -27,6 +28,7 @@ export type OfferItem = {
tokens: {
token: string
tokenId: BigNumberish
amount: number
}[]
}
......@@ -67,7 +69,7 @@ const randomSalt = () => {
return hexZeroPad(randomHex, 64)
}
const encodeItemData = (data: { token: string; tokenId: BigNumberish }[]) => {
const encodeItemData = (data: { token: string; tokenId: BigNumberish; amount: number }[]) => {
return defaultAbiCoder.encode([dataParamType], [data])
}
......@@ -105,11 +107,16 @@ export const encodeOrder = (order: Order): string => {
return defaultAbiCoder.encode([orderParamType], [order])
}
export const createSellOrder = (user: string, deadline: number, items: OfferItem[]): Order => {
export const createSellOrder = (
user: string,
deadline: number,
items: OfferItem[],
nftStandard: NftStandard = NftStandard.Erc721
): Order => {
const salt = randomSalt()
const network = 1 // mainnet
const intent = 1 // INTENT_SELL
const delegateType = 1 // DELEGATION_TYPE_ERC721
const delegateType = nftStandard === NftStandard.Erc721 ? 1 : 2
const currency = AddressZero // ETH
return {
salt,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment