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

refactor: NFT Assets GraphQL Integration (#4928)

* add demo Asset Fetcher

* new file

* update fetcher

* update query name

* beginning integration type

* uncomment

* working mutant apes

* comment out debug logging

* pass in inputs to query

* update collections to handle inf scroll

* paginated query first attempt

* wrapped assetQuery

* building pagination, needs spread

* working pagination

* working sort

* use cacheconfig

* change query source in Collection page

* passed in filters

* fetch schema from main endpoint

* delete unused relayenv

* rename token_url

* easy GenieAsset refactoring

* add rarity

* update price info

* remove logging

* remove redundancy

* refactor usd price fetching for assets

* update standard and address

* remove unused cacheconfig

* dont repeat ethprice calc

* unmemo bools

* reduce duplicated usd price logic

* cleanup imports

* useUsd price hook

* resolve merge conflict
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent bed0b3ab
import ms from 'ms.macro' import ms from 'ms.macro'
import { Variables } from 'react-relay' import { Variables } from 'react-relay'
import { CacheConfig, Environment, Network, RecordSource, RequestParameters, Store } from 'relay-runtime' import { Environment, Network, RecordSource, RequestParameters, Store } from 'relay-runtime'
import RelayQueryResponseCache from 'relay-runtime/lib/network/RelayQueryResponseCache' import RelayQueryResponseCache from 'relay-runtime/lib/network/RelayQueryResponseCache'
import fetchGraphQL from './fetchGraphQL' import fetchGraphQL from './fetchGraphQL'
...@@ -10,17 +10,13 @@ const size = 250 ...@@ -10,17 +10,13 @@ const size = 250
const ttl = ms`5m` const ttl = ms`5m`
export const cache = new RelayQueryResponseCache({ size, ttl }) export const cache = new RelayQueryResponseCache({ size, ttl })
const fetchQuery = async function wrappedFetchQuery( const fetchQuery = async function wrappedFetchQuery(params: RequestParameters, variables: Variables) {
params: RequestParameters,
variables: Variables,
cacheConfig: CacheConfig
) {
const queryID = params.name const queryID = params.name
const cachedData = cache.get(queryID, variables) const cachedData = cache.get(queryID, variables)
if (cachedData !== null) return cachedData if (cachedData !== null) return cachedData
return fetchGraphQL(params, variables, cacheConfig).then((data) => { return fetchGraphQL(params, variables).then((data) => {
if (params.operationKind !== 'mutation') { if (params.operationKind !== 'mutation') {
cache.set(queryID, variables, data) cache.set(queryID, variables, data)
} }
......
import { Variables } from 'react-relay' import { Variables } from 'react-relay'
import { CacheConfig, GraphQLResponse, RequestParameters } from 'relay-runtime' import { GraphQLResponse, RequestParameters } from 'relay-runtime'
const URL = process.env.REACT_APP_AWS_API_ENDPOINT const TOKEN_URL = process.env.REACT_APP_AWS_API_ENDPOINT
const NFT_URL = process.env.REACT_APP_NFT_AWS_API_ENDPOINT ?? '' const NFT_URL = process.env.REACT_APP_NFT_AWS_API_ENDPOINT ?? ''
if (!URL) { if (!TOKEN_URL) {
throw new Error('AWS URL MISSING FROM ENVIRONMENT') throw new Error('AWS URL MISSING FROM ENVIRONMENT')
} }
...@@ -16,17 +16,18 @@ const nftHeaders = { ...@@ -16,17 +16,18 @@ const nftHeaders = {
'x-api-key': process.env.REACT_APP_NFT_AWS_X_API_KEY ?? '', 'x-api-key': process.env.REACT_APP_NFT_AWS_X_API_KEY ?? '',
} }
const fetchQuery = ( // The issue below prevented using a custom var in metadata to gate which queries are for the nft endpoint vs base endpoint
params: RequestParameters, // This is a temporary solution before the two endpoints merge
variables: Variables, // https://github.com/relay-tools/relay-hooks/issues/215
cacheConfig: CacheConfig const NFT_QUERIES = ['AssetQuery', 'AssetPaginationQuery']
): Promise<GraphQLResponse> => {
const { metadata: { isNFT } = { isNFT: false } } = cacheConfig const fetchQuery = (params: RequestParameters, variables: Variables): Promise<GraphQLResponse> => {
const isNFT = NFT_QUERIES.includes(params.name)
const body = JSON.stringify({ const body = JSON.stringify({
query: params.text, // GraphQL text from input query: params.text, // GraphQL text from input
variables, variables,
}) })
const url = isNFT ? NFT_URL : URL const url = isNFT ? NFT_URL : TOKEN_URL
const headers = isNFT ? nftHeaders : baseHeaders const headers = isNFT ? nftHeaders : baseHeaders
return fetch(url, { method: 'POST', body, headers }) return fetch(url, { method: 'POST', body, headers })
......
import graphql from 'babel-plugin-relay/macro'
import { parseEther } from 'ethers/lib/utils'
import { GenieAsset } from 'nft/types'
import { loadQuery, usePaginationFragment, usePreloadedQuery } from 'react-relay'
import RelayEnvironment from '../RelayEnvironment'
import { AssetPaginationQuery } from './__generated__/AssetPaginationQuery.graphql'
import { AssetQuery, NftAssetsFilterInput, NftAssetSortableField } from './__generated__/AssetQuery.graphql'
const assetPaginationQuery = graphql`
fragment AssetQuery_nftAssets on Query @refetchable(queryName: "AssetPaginationQuery") {
nftAssets(
address: $address
orderBy: $orderBy
asc: $asc
filter: $filter
first: $first
after: $after
last: $last
before: $before
) @connection(key: "AssetQuery_nftAssets") {
edges {
node {
id
name
ownerAddress
image {
url
}
smallImage {
url
}
originalImage {
url
}
tokenId
description
animationUrl
suspiciousFlag
collection {
name
isVerified
image {
url
}
creator {
address
profileImage {
url
}
isVerified
}
nftContracts {
address
standard
}
}
listings(first: 1) {
edges {
node {
address
createdAt
endAt
id
maker
marketplace
marketplaceUrl
orderHash
price {
currency
value
}
quantity
startAt
status
taker
tokenId
type
}
cursor
}
}
rarities {
provider
rank
score
}
metadataUrl
}
}
}
}
`
const assetQuery = graphql`
query AssetQuery(
$address: String!
$orderBy: NftAssetSortableField
$asc: Boolean
$filter: NftAssetsFilterInput
$first: Int
$after: String
$last: Int
$before: String
) {
...AssetQuery_nftAssets
}
`
export function useAssetsQuery(
address: string,
orderBy: NftAssetSortableField,
asc: boolean,
filter: NftAssetsFilterInput,
first?: number,
after?: string,
last?: number,
before?: string
) {
const assetsQueryReference = loadQuery<AssetQuery>(RelayEnvironment, assetQuery, {
address,
orderBy,
asc,
filter,
first,
after,
last,
before,
})
const queryData = usePreloadedQuery<AssetQuery>(assetQuery, assetsQueryReference)
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<AssetPaginationQuery, any>(
assetPaginationQuery,
queryData
)
const assets: GenieAsset[] = data.nftAssets?.edges?.map((queryAsset: { node: any }) => {
const asset = queryAsset.node
const ethPrice = parseEther(asset.listings?.edges[0].node.price.value?.toString() ?? '0').toString()
return {
id: asset.id,
address: asset.collection.nftContracts[0].address,
notForSale: asset.listings === null,
collectionName: asset.collection?.name,
collectionSymbol: asset.collection?.image?.url,
imageUrl: asset.image?.url,
animationUrl: asset.animationUrl,
marketplace: asset.listings?.edges[0].node.marketplace.toLowerCase(),
name: asset.name,
priceInfo: asset.listings
? {
ETHPrice: ethPrice,
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: ethPrice,
}
: undefined,
susFlag: asset.suspiciousFlag,
sellorders: asset.listings?.edges,
smallImageUrl: asset.smallImage?.url,
tokenId: asset.tokenId,
tokenType: asset.collection.nftContracts[0].standard,
// totalCount?: number, // TODO waiting for BE changes
collectionIsVerified: asset.collection?.isVerified,
rarity: {
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
providers: asset.rarities,
},
owner: asset.ownerAddress,
creator: {
profile_img_url: asset.collection?.creator?.profileImage?.url,
address: asset.collection?.creator?.address,
},
metadataUrl: asset.metadataUrl,
}
})
return { assets, hasNext, isLoadingNext, loadNext }
}
...@@ -52,7 +52,7 @@ export const CollectionAsset = ({ ...@@ -52,7 +52,7 @@ export const CollectionAsset = ({
let notForSale = true let notForSale = true
let assetMediaType = AssetMediaType.Image let assetMediaType = AssetMediaType.Image
notForSale = asset.notForSale || BigNumber.from(asset.currentEthPrice ? asset.currentEthPrice : 0).lt(0) notForSale = asset.notForSale || BigNumber.from(asset.priceInfo.ETHPrice ? asset.priceInfo.ETHPrice : 0).lt(0)
if (isAudio(asset.animationUrl)) { if (isAudio(asset.animationUrl)) {
assetMediaType = AssetMediaType.Audio assetMediaType = AssetMediaType.Audio
} else if (isVideo(asset.animationUrl)) { } else if (isVideo(asset.animationUrl)) {
...@@ -96,7 +96,7 @@ export const CollectionAsset = ({ ...@@ -96,7 +96,7 @@ export const CollectionAsset = ({
<Card.PrimaryRow> <Card.PrimaryRow>
<Card.PrimaryDetails> <Card.PrimaryDetails>
<Card.PrimaryInfo>{asset.name ? asset.name : `#${asset.tokenId}`}</Card.PrimaryInfo> <Card.PrimaryInfo>{asset.name ? asset.name : `#${asset.tokenId}`}</Card.PrimaryInfo>
{asset.openseaSusFlag && <Card.Suspicious />} {asset.susFlag && <Card.Suspicious />}
</Card.PrimaryDetails> </Card.PrimaryDetails>
{asset.rarity && provider && provider.rank && ( {asset.rarity && provider && provider.rank && (
<Card.Ranking <Card.Ranking
...@@ -110,7 +110,7 @@ export const CollectionAsset = ({ ...@@ -110,7 +110,7 @@ export const CollectionAsset = ({
<Card.SecondaryRow> <Card.SecondaryRow>
<Card.SecondaryDetails> <Card.SecondaryDetails>
<Card.SecondaryInfo> <Card.SecondaryInfo>
{notForSale ? '' : `${formatWeiToDecimal(asset.currentEthPrice, true)} ETH`} {notForSale ? '' : `${formatWeiToDecimal(asset.priceInfo.ETHPrice, true)} ETH`}
</Card.SecondaryInfo> </Card.SecondaryInfo>
{(asset.marketplace === Markets.NFTX || asset.marketplace === Markets.NFT20) && <Card.Pool />} {(asset.marketplace === Markets.NFTX || asset.marketplace === Markets.NFT20) && <Card.Pool />}
</Card.SecondaryDetails> </Card.SecondaryDetails>
......
...@@ -3,6 +3,10 @@ import { ElementName, Event, EventName } from 'analytics/constants' ...@@ -3,6 +3,10 @@ import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent' import { TraceEvent } from 'analytics/TraceEvent'
import clsx from 'clsx' import clsx from 'clsx'
import { loadingAnimation } from 'components/Loader/styled' import { loadingAnimation } from 'components/Loader/styled'
import { parseEther } from 'ethers/lib/utils'
import { NftGraphQlVariant, useNftGraphQlFlag } from 'featureFlags/flags/nftGraphQl'
import { NftAssetTraitInput, NftMarketplace } from 'graphql/data/nft/__generated__/AssetQuery.graphql'
import { useAssetsQuery } from 'graphql/data/nft/Asset'
import useDebounce from 'hooks/useDebounce' import useDebounce from 'hooks/useDebounce'
import { AnimatedBox, Box } from 'nft/components/Box' import { AnimatedBox, Box } from 'nft/components/Box'
import { CollectionSearch, FilterButton } from 'nft/components/collection' import { CollectionSearch, FilterButton } from 'nft/components/collection'
...@@ -17,6 +21,7 @@ import { ...@@ -17,6 +21,7 @@ import {
CollectionFilters, CollectionFilters,
initialCollectionFilterState, initialCollectionFilterState,
SortBy, SortBy,
SortByQueries,
useBag, useBag,
useCollectionFilters, useCollectionFilters,
useFiltersExpanded, useFiltersExpanded,
...@@ -42,6 +47,11 @@ import { marketPlaceItems } from './MarketplaceSelect' ...@@ -42,6 +47,11 @@ import { marketPlaceItems } from './MarketplaceSelect'
import { Sweep } from './Sweep' import { Sweep } from './Sweep'
import { TraitChip } from './TraitChip' import { TraitChip } from './TraitChip'
const EmptyCollectionWrapper = styled.div`
display: block;
textalign: center;
`
interface CollectionNftsProps { interface CollectionNftsProps {
contractAddress: string contractAddress: string
collectionStats: GenieCollection collectionStats: GenieCollection
...@@ -106,6 +116,8 @@ export const LoadingButton = styled.div` ...@@ -106,6 +116,8 @@ export const LoadingButton = styled.div`
background-size: 400%; background-size: 400%;
` `
const DEFAULT_ASSET_QUERY_AMOUNT = 25
export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerified }: CollectionNftsProps) => { export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerified }: CollectionNftsProps) => {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const traits = useCollectionFilters((state) => state.traits) const traits = useCollectionFilters((state) => state.traits)
...@@ -130,6 +142,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -130,6 +142,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const reset = useCollectionFilters((state) => state.reset) const reset = useCollectionFilters((state) => state.reset)
const setMin = useCollectionFilters((state) => state.setMinPrice) const setMin = useCollectionFilters((state) => state.setMinPrice)
const setMax = useCollectionFilters((state) => state.setMaxPrice) const setMax = useCollectionFilters((state) => state.setMaxPrice)
const isNftGraphQl = useNftGraphQlFlag() === NftGraphQlVariant.Enabled
const toggleBag = useBag((state) => state.toggleBag) const toggleBag = useBag((state) => state.toggleBag)
const bagExpanded = useBag((state) => state.bagExpanded) const bagExpanded = useBag((state) => state.bagExpanded)
...@@ -197,7 +210,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -197,7 +210,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
}, },
{ {
getNextPageParam: (lastPage, pages) => { getNextPageParam: (lastPage, pages) => {
return lastPage?.flat().length === 25 ? pages.length : null return lastPage?.flat().length === DEFAULT_ASSET_QUERY_AMOUNT ? pages.length : null
}, },
refetchOnReconnect: false, refetchOnReconnect: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
...@@ -205,10 +218,30 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -205,10 +218,30 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
refetchInterval: 5000, refetchInterval: 5000,
} }
) )
const {
useEffect(() => { assets: nftQueryAssets,
setIsCollectionNftsLoading(isLoading) loadNext,
}, [isLoading, setIsCollectionNftsLoading]) hasNext,
isLoadingNext,
} = useAssetsQuery(
isNftGraphQl ? contractAddress : '',
SortByQueries[sortBy].field,
SortByQueries[sortBy].asc,
{
listed: buyNow,
marketplaces: markets.length > 0 ? markets.map((market) => market.toUpperCase() as NftMarketplace) : undefined,
maxPrice: debouncedMaxPrice ? parseEther(debouncedMaxPrice).toString() : undefined,
minPrice: debouncedMinPrice ? parseEther(debouncedMinPrice).toString() : undefined,
tokenSearchQuery: debouncedSearchByNameText,
traits:
traits.length > 0
? traits.map((trait) => {
return { name: trait.trait_type, values: [trait.trait_value] } as unknown as NftAssetTraitInput
})
: undefined,
},
DEFAULT_ASSET_QUERY_AMOUNT
)
const [uniformHeight, setUniformHeight] = useState<UniformHeight>(UniformHeights.unset) const [uniformHeight, setUniformHeight] = useState<UniformHeight>(UniformHeights.unset)
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>() const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
...@@ -217,12 +250,24 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -217,12 +250,24 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const isMobile = useIsMobile() const isMobile = useIsMobile()
const collectionNfts = useMemo(() => { const collectionNfts = useMemo(() => {
if (!collectionAssets || !AssetsFetchSuccess) return undefined if (
(isNftGraphQl && !nftQueryAssets && !isLoadingNext) ||
(!isNftGraphQl && !collectionAssets) ||
!AssetsFetchSuccess
)
return undefined
return collectionAssets.pages.flat() return isNftGraphQl ? nftQueryAssets : collectionAssets?.pages.flat()
}, [collectionAssets, AssetsFetchSuccess]) }, [AssetsFetchSuccess, collectionAssets, isLoadingNext, isNftGraphQl, nftQueryAssets])
const loadingAssets = useMemo(() => <>{new Array(25).fill(<CollectionAssetLoading />)}</>, []) const wrappedLoadingState = isNftGraphQl ? isLoadingNext : isLoading
const wrappedHasNext = isNftGraphQl ? hasNext : hasNextPage ?? false
useEffect(() => {
setIsCollectionNftsLoading(wrappedLoadingState)
}, [wrappedLoadingState, setIsCollectionNftsLoading])
const loadingAssets = useMemo(() => <>{new Array(DEFAULT_ASSET_QUERY_AMOUNT).fill(<CollectionAssetLoading />)}</>, [])
const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionNfts) const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionNfts)
const sortDropDownOptions: DropDownOption[] = useMemo( const sortDropDownOptions: DropDownOption[] = useMemo(
...@@ -469,25 +514,25 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -469,25 +514,25 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
</Box> </Box>
</AnimatedBox> </AnimatedBox>
<InfiniteScroll <InfiniteScroll
next={fetchNextPage} next={() => (isNftGraphQl ? loadNext(DEFAULT_ASSET_QUERY_AMOUNT) : fetchNextPage())}
hasMore={hasNextPage ?? false} hasMore={wrappedHasNext}
loader={hasNextPage ? loadingAssets : null} loader={wrappedHasNext ? loadingAssets : null}
dataLength={collectionNfts?.length ?? 0} dataLength={collectionNfts?.length ?? 0}
style={{ overflow: 'unset' }} style={{ overflow: 'unset' }}
className={hasNfts || isLoading ? styles.assetList : undefined} className={hasNfts || wrappedLoadingState ? styles.assetList : undefined}
> >
{hasNfts {hasNfts ? (
? Nfts Nfts
: isLoading ) : wrappedLoadingState ? (
? loadingAssets loadingAssets
: !isLoading && ( ) : (
<Center width="full" color="textSecondary" style={{ height: '60vh' }}> <Center width="full" color="textSecondary" style={{ height: '60vh' }}>
<div style={{ display: 'block', textAlign: 'center' }}> <EmptyCollectionWrapper>
<p className={headlineMedium}>No NFTS found</p> <p className={headlineMedium}>No NFTS found</p>
<Box className={clsx(bodySmall, buttonTextMedium)} color="blue" cursor="pointer"> <Box className={clsx(bodySmall, buttonTextMedium)} color="blue" cursor="pointer">
View full collection View full collection
</Box> </Box>
</div> </EmptyCollectionWrapper>
</Center> </Center>
)} )}
</InfiniteScroll> </InfiniteScroll>
......
...@@ -265,21 +265,21 @@ export const Sweep = ({ contractAddress, collectionStats, minPrice, maxPrice, sh ...@@ -265,21 +265,21 @@ export const Sweep = ({ contractAddress, collectionStats, minPrice, maxPrice, sh
let jointCollections = [...nftxCollectionAssets, ...nft20CollectionAssets] let jointCollections = [...nftxCollectionAssets, ...nft20CollectionAssets]
jointCollections.forEach((asset) => { jointCollections.forEach((asset) => {
if (!asset.openseaSusFlag) { if (!asset.susFlag) {
const isNFTX = asset.marketplace === Markets.NFTX const isNFTX = asset.marketplace === Markets.NFTX
asset.currentEthPrice = calcPoolPrice(asset, isNFTX ? counterNFTX : counterNFT20) asset.priceInfo.ETHPrice = calcPoolPrice(asset, isNFTX ? counterNFTX : counterNFT20)
BigNumber.from(asset.currentEthPrice).gte(0) && (isNFTX ? counterNFTX++ : counterNFT20++) BigNumber.from(asset.priceInfo.ETHPrice).gte(0) && (isNFTX ? counterNFTX++ : counterNFT20++)
} }
}) })
jointCollections = collectionAssets.concat(jointCollections) jointCollections = collectionAssets.concat(jointCollections)
jointCollections.sort((a, b) => { jointCollections.sort((a, b) => {
return BigNumber.from(a.currentEthPrice).gt(BigNumber.from(b.currentEthPrice)) ? 1 : -1 return BigNumber.from(a.priceInfo.ETHPrice).gt(BigNumber.from(b.priceInfo.ETHPrice)) ? 1 : -1
}) })
let validAssets = jointCollections.filter( let validAssets = jointCollections.filter(
(asset) => BigNumber.from(asset.currentEthPrice).gte(0) && !asset.openseaSusFlag (asset) => BigNumber.from(asset.priceInfo.ETHPrice).gte(0) && !asset.susFlag
) )
validAssets = validAssets.slice( validAssets = validAssets.slice(
......
...@@ -14,6 +14,7 @@ import { themeVars } from 'nft/css/sprinkles.css' ...@@ -14,6 +14,7 @@ import { themeVars } from 'nft/css/sprinkles.css'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { useTimeout } from 'nft/hooks/useTimeout' import { useTimeout } from 'nft/hooks/useTimeout'
import { CollectionInfoForAsset, GenieAsset, SellOrder } from 'nft/types' import { CollectionInfoForAsset, GenieAsset, SellOrder } from 'nft/types'
import { useUsdPrice } from 'nft/utils'
import { shortenAddress } from 'nft/utils/address' import { shortenAddress } from 'nft/utils/address'
import { formatEthPrice } from 'nft/utils/currency' import { formatEthPrice } from 'nft/utils/currency'
import { isAssetOwnedByUser } from 'nft/utils/isAssetOwnedByUser' import { isAssetOwnedByUser } from 'nft/utils/isAssetOwnedByUser'
...@@ -176,6 +177,8 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { ...@@ -176,6 +177,8 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
} }
}, [asset, address, provider]) }, [asset, address, provider])
const USDPrice = useUsdPrice(asset)
return ( return (
<AnimatedBox <AnimatedBox
style={{ style={{
...@@ -270,7 +273,7 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { ...@@ -270,7 +273,7 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
</Row> </Row>
</Row> </Row>
<Row as="h1" marginTop="0" marginBottom="12" gap="2" className={headlineMedium}> <Row as="h1" marginTop="0" marginBottom="12" gap="2" className={headlineMedium}>
{asset.openseaSusFlag && ( {asset.susFlag && (
<Box marginTop="8"> <Box marginTop="8">
<MouseoverTooltip text={<Box fontWeight="normal">{SUSPICIOUS_TEXT}</Box>}> <MouseoverTooltip text={<Box fontWeight="normal">{SUSPICIOUS_TEXT}</Box>}>
<SuspiciousIcon height="30" width="30" viewBox="0 0 16 17" /> <SuspiciousIcon height="30" width="30" viewBox="0 0 16 17" />
...@@ -366,9 +369,11 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { ...@@ -366,9 +369,11 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
<Row as="span" className={subhead} color="textPrimary"> <Row as="span" className={subhead} color="textPrimary">
{formatEthPrice(asset.priceInfo.ETHPrice)} <Eth2Icon /> {formatEthPrice(asset.priceInfo.ETHPrice)} <Eth2Icon />
</Row> </Row>
{USDPrice && (
<Box as="span" color="textSecondary" className={bodySmall}> <Box as="span" color="textSecondary" className={bodySmall}>
${toSignificant(asset.priceInfo.USDPrice)} ${toSignificant(USDPrice)}
</Box> </Box>
)}
</Row> </Row>
{asset.sellorders?.[0].orderClosingDate ? <CountdownTimer sellOrder={asset.sellorders[0]} /> : null} {asset.sellorders?.[0].orderClosingDate ? <CountdownTimer sellOrder={asset.sellorders[0]} /> : null}
</Column> </Column>
...@@ -413,7 +418,7 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { ...@@ -413,7 +418,7 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
tokenId={asset.tokenId} tokenId={asset.tokenId}
tokenType={asset.tokenType} tokenType={asset.tokenType}
blockchain="Ethereum" blockchain="Ethereum"
metadataUrl={asset.externalLink} metadataUrl={asset.metadataUrl}
totalSupply={collection.totalSupply} totalSupply={collection.totalSupply}
/> />
)} )}
......
...@@ -2,7 +2,7 @@ import { useWeb3React } from '@web3-react/core' ...@@ -2,7 +2,7 @@ import { useWeb3React } from '@web3-react/core'
import { CancelListingIcon, MinusIcon, PlusIcon } from 'nft/components/icons' import { CancelListingIcon, MinusIcon, PlusIcon } from 'nft/components/icons'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { CollectionInfoForAsset, GenieAsset, TokenType } from 'nft/types' import { CollectionInfoForAsset, GenieAsset, TokenType } from 'nft/types'
import { ethNumberStandardFormatter, formatEthPrice, getMarketplaceIcon, timeLeft } from 'nft/utils' import { ethNumberStandardFormatter, formatEthPrice, getMarketplaceIcon, timeLeft, useUsdPrice } from 'nft/utils'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
...@@ -111,6 +111,7 @@ const DiscoveryContainer = styled.div` ...@@ -111,6 +111,7 @@ const DiscoveryContainer = styled.div`
export const OwnerContainer = ({ asset }: { asset: GenieAsset }) => { export const OwnerContainer = ({ asset }: { asset: GenieAsset }) => {
const listing = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined const listing = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined
const expirationDate = listing ? new Date(listing.orderClosingDate) : undefined const expirationDate = listing ? new Date(listing.orderClosingDate) : undefined
const USDPrice = useUsdPrice(asset)
const navigate = useNavigate() const navigate = useNavigate()
...@@ -129,9 +130,11 @@ export const OwnerContainer = ({ asset }: { asset: GenieAsset }) => { ...@@ -129,9 +130,11 @@ export const OwnerContainer = ({ asset }: { asset: GenieAsset }) => {
<ThemedText.MediumHeader fontSize={'28px'} lineHeight={'36px'}> <ThemedText.MediumHeader fontSize={'28px'} lineHeight={'36px'}>
{formatEthPrice(asset.priceInfo.ETHPrice)} {formatEthPrice(asset.priceInfo.ETHPrice)}
</ThemedText.MediumHeader> </ThemedText.MediumHeader>
{USDPrice && (
<ThemedText.BodySecondary lineHeight={'24px'}> <ThemedText.BodySecondary lineHeight={'24px'}>
{ethNumberStandardFormatter(asset.priceInfo.USDPrice, true, true)} {ethNumberStandardFormatter(USDPrice, true, true)}
</ThemedText.BodySecondary> </ThemedText.BodySecondary>
)}
</> </>
) : ( ) : (
<ThemedText.BodySecondary fontSize="14px" lineHeight={'20px'}> <ThemedText.BodySecondary fontSize="14px" lineHeight={'20px'}>
...@@ -189,6 +192,8 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) ...@@ -189,6 +192,8 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
const itemsInBag = useBag((s) => s.itemsInBag) const itemsInBag = useBag((s) => s.itemsInBag)
const addAssetsToBag = useBag((s) => s.addAssetsToBag) const addAssetsToBag = useBag((s) => s.addAssetsToBag)
const removeAssetsFromBag = useBag((s) => s.removeAssetsFromBag) const removeAssetsFromBag = useBag((s) => s.removeAssetsFromBag)
const USDPrice = useUsdPrice(asset)
const isErc1555 = asset.tokenType === TokenType.ERC1155 const isErc1555 = asset.tokenType === TokenType.ERC1155
const { quantity, assetInBag } = useMemo(() => { const { quantity, assetInBag } = useMemo(() => {
...@@ -223,9 +228,11 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) ...@@ -223,9 +228,11 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
<ThemedText.MediumHeader fontSize={'28px'} lineHeight={'36px'}> <ThemedText.MediumHeader fontSize={'28px'} lineHeight={'36px'}>
{formatEthPrice(asset.priceInfo.ETHPrice)} {formatEthPrice(asset.priceInfo.ETHPrice)}
</ThemedText.MediumHeader> </ThemedText.MediumHeader>
{USDPrice && (
<ThemedText.BodySecondary lineHeight={'24px'}> <ThemedText.BodySecondary lineHeight={'24px'}>
{ethNumberStandardFormatter(asset.priceInfo.USDPrice, true, true)} {ethNumberStandardFormatter(USDPrice, true, true)}
</ThemedText.BodySecondary> </ThemedText.BodySecondary>
)}
</PriceRow> </PriceRow>
{expirationDate && ( {expirationDate && (
<ThemedText.BodySecondary fontSize={'14px'}>Sale ends: {timeLeft(expirationDate)}</ThemedText.BodySecondary> <ThemedText.BodySecondary fontSize={'14px'}>Sale ends: {timeLeft(expirationDate)}</ThemedText.BodySecondary>
......
import { NftAssetSortableField } from 'graphql/data/nft/__generated__/AssetPaginationQuery.graphql'
import create from 'zustand' import create from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
...@@ -14,6 +15,16 @@ export const SortByPointers = { ...@@ -14,6 +15,16 @@ export const SortByPointers = {
[SortBy.RareToCommon]: 'rare', [SortBy.RareToCommon]: 'rare',
[SortBy.CommonToRare]: 'common', [SortBy.CommonToRare]: 'common',
} }
interface QueryInfo {
field: NftAssetSortableField
asc: boolean
}
export const SortByQueries = {
[SortBy.HighToLow]: { field: 'PRICE', asc: false } as QueryInfo,
[SortBy.LowToHigh]: { field: 'PRICE', asc: true } as QueryInfo,
[SortBy.RareToCommon]: { field: 'RARITY', asc: true } as QueryInfo,
[SortBy.CommonToRare]: { field: 'RARITY', asc: false } as QueryInfo,
}
export type Trait = { export type Trait = {
trait_type: string trait_type: string
......
...@@ -53,13 +53,13 @@ const buildRouteItem = (item: GenieAsset): RouteItem => { ...@@ -53,13 +53,13 @@ const buildRouteItem = (item: GenieAsset): RouteItem => {
id: item.id, id: item.id,
symbol: item.priceInfo.baseAsset, symbol: item.priceInfo.baseAsset,
name: item.name, name: item.name,
decimals: item.decimals || 0, // 0 for fungible items decimals: parseFloat(item.priceInfo.baseDecimals),
address: item.address, address: item.address,
tokenType: item.tokenType, tokenType: item.tokenType,
tokenId: item.tokenId, tokenId: item.tokenId,
marketplace: item.marketplace, marketplace: item.marketplace,
collectionName: item.collectionName, collectionName: item.collectionName,
amount: item.amount || 1, // default 1 for a single asset amount: 1,
priceInfo: { priceInfo: {
basePrice: item.priceInfo.basePrice, basePrice: item.priceInfo.basePrice,
baseAsset: item.priceInfo.baseAsset, baseAsset: item.priceInfo.baseAsset,
......
...@@ -72,7 +72,7 @@ export interface AssetSellOrder { ...@@ -72,7 +72,7 @@ export interface AssetSellOrder {
export interface Rarity { export interface Rarity {
primaryProvider: string primaryProvider: string
providers: { provider: string; rank: number; url: string; score: number }[] providers: { provider: string; rank: number; url?: string; score: number }[]
} }
export interface GenieAsset { export interface GenieAsset {
...@@ -81,27 +81,22 @@ export interface GenieAsset { ...@@ -81,27 +81,22 @@ export interface GenieAsset {
notForSale: boolean notForSale: boolean
collectionName: string collectionName: string
collectionSymbol: string collectionSymbol: string
currentEthPrice: string
currentUsdPrice: string
imageUrl: string imageUrl: string
animationUrl: string animationUrl: string
marketplace: Markets marketplace: Markets
name: string name: string
priceInfo: PriceInfo priceInfo: PriceInfo
openseaSusFlag: boolean susFlag: boolean
sellorders: SellOrder[] sellorders: SellOrder[]
smallImageUrl: string smallImageUrl: string
tokenId: string tokenId: string
tokenType: TokenType tokenType: TokenType
url: string
totalCount?: number // The totalCount from the query to /assets totalCount?: number // The totalCount from the query to /assets
amount?: number
decimals?: number
collectionIsVerified?: boolean collectionIsVerified?: boolean
rarity?: Rarity rarity?: Rarity
owner: string owner: string
creator: OpenSeaUser creator: OpenSeaUser
externalLink: string metadataUrl: string
traits?: { traits?: {
trait_type: string trait_type: string
value: string value: string
......
...@@ -14,15 +14,13 @@ export const buildActivityAsset = (event: ActivityEvent, collectionName: string, ...@@ -14,15 +14,13 @@ export const buildActivityAsset = (event: ActivityEvent, collectionName: string,
return { return {
address: event.collectionAddress, address: event.collectionAddress,
collectionName, collectionName,
currentEthPrice: event.price,
imageUrl: event.tokenMetadata?.imageUrl, imageUrl: event.tokenMetadata?.imageUrl,
marketplace: event.marketplace, marketplace: event.marketplace,
name: event.tokenMetadata?.name, name: event.tokenMetadata?.name,
tokenId: event.tokenId, tokenId: event.tokenId,
openseaSusFlag: event.tokenMetadata?.suspiciousFlag, susFlag: event.tokenMetadata?.suspiciousFlag,
smallImageUrl: event.tokenMetadata?.smallImageUrl, smallImageUrl: event.tokenMetadata?.smallImageUrl,
collectionSymbol: event.symbol, collectionSymbol: event.symbol,
currentUsdPrice: assetUsdPrice,
priceInfo: { priceInfo: {
USDPrice: assetUsdPrice, USDPrice: assetUsdPrice,
ETHPrice: event.price, ETHPrice: event.price,
......
...@@ -113,7 +113,7 @@ export const recalculateBagUsingPooledAssets = (uncheckedItemsInBag: BagItem[]) ...@@ -113,7 +113,7 @@ export const recalculateBagUsingPooledAssets = (uncheckedItemsInBag: BagItem[])
if (isPriceChangedAsset && item.asset.updatedPriceInfo) if (isPriceChangedAsset && item.asset.updatedPriceInfo)
item.asset.updatedPriceInfo.ETHPrice = item.asset.updatedPriceInfo.basePrice = calculatedPrice item.asset.updatedPriceInfo.ETHPrice = item.asset.updatedPriceInfo.basePrice = calculatedPrice
else item.asset.currentEthPrice = item.asset.priceInfo.ETHPrice = calculatedPrice else item.asset.priceInfo.ETHPrice = calculatedPrice
} }
}) })
......
import { formatEther } from '@ethersproject/units'
import { GenieAsset } from 'nft/types'
import { useQuery } from 'react-query'
export enum Currency { export enum Currency {
ETH = 'ETH', ETH = 'ETH',
LOOKS = 'LOOKS', LOOKS = 'LOOKS',
...@@ -13,3 +17,8 @@ export const fetchPrice = async (currency: Currency = Currency.ETH): Promise<num ...@@ -13,3 +17,8 @@ export const fetchPrice = async (currency: Currency = Currency.ETH): Promise<num
return return
} }
} }
export function useUsdPrice(asset: GenieAsset): string {
const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {})
return fetchedPriceData ? (parseFloat(formatEther(asset.priceInfo.ETHPrice)) * fetchedPriceData).toString() : ''
}
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