feat: Migrate NFT Activity to GraphQL (#6103)

* add new activity query

* add nftactivity hook and fix query

* converted activity type to accept optionals, hook should return activity type

* single asset activity

* relaystylepagination

* working on local endpoint

* working timestamps

* updated endpoint

* use correct env var

* undo asset testing

* forgot last

* undo more testing

* don't break on null address

* working loading state

* better loading states

* handle marketplace type update

* remove types

* properly format price and reduce redundancy

* handle null price

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2023-03-14 14:07:32 -07:00 committed by GitHub
parent bc251230da
commit 18939aa871
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 418 additions and 142 deletions

@ -19,6 +19,7 @@ export const apolloClient = new ApolloClient({
fields: {
nftBalances: relayStylePagination(),
nftAssets: relayStylePagination(),
nftActivity: relayStylePagination(),
// tell apollo client how to reference Token items in the cache after being fetched by queries that return Token[]
token: {
read(_, { args, toReference }): Reference | undefined {

@ -0,0 +1,140 @@
import { WatchQueryFetchPolicy } from '@apollo/client'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import gql from 'graphql-tag'
import { ActivityEvent } from 'nft/types'
import { useCallback, useMemo } from 'react'
import { NftActivityFilterInput, useNftActivityQuery } from '../__generated__/types-and-hooks'
gql`
query NftActivity($filter: NftActivityFilterInput, $after: String, $first: Int) {
nftActivity(filter: $filter, after: $after, first: $first) {
edges {
node {
id
address
tokenId
asset {
id
metadataUrl
image {
id
url
}
smallImage {
id
url
}
name
rarities {
id
provider
rank
score
}
suspiciousFlag
nftContract {
id
standard
}
collection {
id
image {
id
url
}
}
}
type
marketplace
fromAddress
toAddress
transactionHash
price {
id
value
}
orderStatus
quantity
url
timestamp
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
`
export function useNftActivity(filter: NftActivityFilterInput, first?: number, fetchPolicy?: WatchQueryFetchPolicy) {
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const { data, loading, fetchMore, error } = useNftActivityQuery({
variables: {
filter,
first,
},
skip: !isNftGraphqlEnabled,
fetchPolicy,
})
const hasNext = data?.nftActivity?.pageInfo?.hasNextPage
const loadMore = useCallback(
() =>
fetchMore({
variables: {
after: data?.nftActivity?.pageInfo?.endCursor,
},
}),
[data, fetchMore]
)
const nftActivity: ActivityEvent[] | undefined = useMemo(
() =>
data?.nftActivity?.edges?.map((queryActivity) => {
const activity = queryActivity?.node
const asset = activity?.asset
return {
collectionAddress: activity.address,
tokenId: activity.tokenId,
tokenMetadata: {
name: asset?.name,
imageUrl: asset?.image?.url,
smallImageUrl: asset?.smallImage?.url,
metadataUrl: asset?.metadataUrl,
rarity: {
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
providers: asset?.rarities?.map((rarity) => {
return {
...rarity,
provider: 'Rarity Sniper',
}
}),
},
suspiciousFlag: asset?.suspiciousFlag,
standard: asset?.nftContract?.standard,
},
eventType: activity.type,
marketplace: activity.marketplace,
fromAddress: activity.fromAddress,
toAddress: activity.toAddress,
transactionHash: activity.transactionHash,
orderStatus: activity.orderStatus,
price: activity.price?.value.toString(),
symbol: asset?.collection?.image?.url,
quantity: activity.quantity,
url: activity.url,
eventTimestamp: activity.timestamp * 1000,
}
}),
[data]
)
return useMemo(
() => ({ nftActivity, hasNext, loadMore, loading, error }),
[hasNext, loadMore, loading, nftActivity, error]
)
}

@ -1,4 +1,7 @@
import { OpacityHoverState } from 'components/Common'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { NftActivityType } from 'graphql/data/__generated__/types-and-hooks'
import { useNftActivity } from 'graphql/data/nft/NftActivity'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { themeVars, vars } from 'nft/css/sprinkles.css'
@ -6,7 +9,7 @@ import { useBag, useIsMobile } from 'nft/hooks'
import { ActivityFetcher } from 'nft/queries/genie/ActivityFetcher'
import { ActivityEvent, ActivityEventResponse, ActivityEventType } from 'nft/types'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useCallback, useEffect, useReducer, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { useInfiniteQuery } from 'react-query'
import { useIsDarkMode } from 'state/user/hooks'
@ -63,6 +66,7 @@ export const reduceFilters = (state: typeof initialFilterState, action: { eventT
const baseHref = (event: ActivityEvent) => `/#/nfts/asset/${event.collectionAddress}/${event.tokenId}?origin=activity`
export const Activity = ({ contractAddress, rarityVerified, collectionName, chainId }: ActivityProps) => {
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState)
const {
@ -102,11 +106,33 @@ export const Activity = ({ contractAddress, rarityVerified, collectionName, chai
}
)
const events = useMemo(
() => (isSuccess ? eventsData?.pages.map((page) => page.events).flat() : null),
[isSuccess, eventsData]
const {
nftActivity: gqlEventsData,
hasNext,
loadMore,
loading,
} = useNftActivity(
{
activityTypes: Object.keys(activeFilters)
.map((key) => key as NftActivityType)
.filter((key) => activeFilters[key]),
address: contractAddress,
},
25
)
const { events, gatedHasNext, gatedLoadMore, gatedLoading, gatedIsLoadingMore } = {
events: isNftGraphqlEnabled
? gqlEventsData
: isSuccess
? eventsData?.pages.map((page) => page.events).flat()
: undefined,
gatedHasNext: isNftGraphqlEnabled ? hasNext : hasNextPage,
gatedLoadMore: isNftGraphqlEnabled ? loadMore : fetchNextPage,
gatedLoading: isNftGraphqlEnabled ? loading : isLoading,
gatedIsLoadingMore: isNftGraphqlEnabled ? hasNext && gqlEventsData?.length : isFetchingNextPage,
}
const itemsInBag = useBag((state) => state.itemsInBag)
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
@ -147,51 +173,63 @@ export const Activity = ({ contractAddress, rarityVerified, collectionName, chai
<Filter eventType={ActivityEventType.Sale} />
<Filter eventType={ActivityEventType.Transfer} />
</Row>
{isLoading && <ActivityLoader />}
{events && (
<Column marginTop="36">
<HeaderRow />
<InfiniteScroll
next={fetchNextPage}
hasMore={!!hasNextPage}
loader={isFetchingNextPage ? <ActivityPageLoader rowCount={2} /> : null}
dataLength={events?.length ?? 0}
style={{ overflow: 'unset' }}
>
{events.map((event, i) => (
<Box as="a" data-testid="nft-activity-row" href={baseHref(event)} className={styles.eventRow} key={i}>
<ItemCell
event={event}
rarityVerified={rarityVerified}
collectionName={collectionName}
eventTimestamp={event.eventTimestamp}
isMobile={isMobile}
/>
<EventCell
eventType={event.eventType}
eventTimestamp={event.eventTimestamp}
eventTransactionHash={event.transactionHash}
price={event.price}
isMobile={isMobile}
/>
<PriceCell marketplace={event.marketplace} price={event.price} />
<AddressCell address={event.fromAddress} chainId={chainId} />
<AddressCell address={event.toAddress} chainId={chainId} desktopLBreakpoint />
<BuyCell
event={event}
collectionName={collectionName}
selectAsset={addAssetsToBag}
removeAsset={removeAssetsFromBag}
itemsInBag={itemsInBag}
cartExpanded={cartExpanded}
toggleCart={toggleCart}
isMobile={isMobile}
ethPriceInUSD={ethPriceInUSD}
/>
</Box>
))}
</InfiniteScroll>
</Column>
{gatedLoading ? (
<ActivityLoader />
) : (
events && (
<Column marginTop="36">
<HeaderRow />
<InfiniteScroll
next={gatedLoadMore}
hasMore={!!gatedHasNext}
loader={gatedIsLoadingMore ? <ActivityPageLoader rowCount={2} /> : null}
dataLength={events?.length ?? 0}
style={{ overflow: 'unset' }}
>
{events.map(
(event, i) =>
event.eventType && (
<Box
as="a"
data-testid="nft-activity-row"
href={baseHref(event)}
className={styles.eventRow}
key={i}
>
<ItemCell
event={event}
rarityVerified={rarityVerified}
collectionName={collectionName}
eventTimestamp={event.eventTimestamp}
isMobile={isMobile}
/>
<EventCell
eventType={event.eventType}
eventTimestamp={event.eventTimestamp}
eventTransactionHash={event.transactionHash}
price={event.price}
isMobile={isMobile}
/>
<PriceCell marketplace={event.marketplace} price={event.price} />
<AddressCell address={event.fromAddress} chainId={chainId} />
<AddressCell address={event.toAddress} chainId={chainId} desktopLBreakpoint />
<BuyCell
event={event}
collectionName={collectionName}
selectAsset={addAssetsToBag}
removeAsset={removeAssetsFromBag}
itemsInBag={itemsInBag}
cartExpanded={cartExpanded}
toggleCart={toggleCart}
isMobile={isMobile}
ethPriceInUSD={ethPriceInUSD}
/>
</Box>
)
)}
</InfiniteScroll>
</Column>
)
)}
</Box>
)

@ -2,6 +2,8 @@ import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfacePageName, NFTEventName } from '@uniswap/analytics-events'
import { ChainId } from '@uniswap/smart-order-router'
import { MouseoverTooltip } from 'components/Tooltip'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { NftActivityType, OrderStatus } from 'graphql/data/__generated__/types-and-hooks'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import {
@ -14,18 +16,17 @@ import {
} from 'nft/components/icons'
import {
ActivityEvent,
ActivityEventType,
ActivityEventTypeDisplay,
BagItem,
GenieAsset,
Markets,
OrderStatus,
Rarity,
TokenMetadata,
TokenRarity,
} from 'nft/types'
import { shortenAddress } from 'nft/utils/address'
import { buildActivityAsset } from 'nft/utils/buildActivityAsset'
import { formatEthPrice } from 'nft/utils/currency'
import { formatEth, formatEthPrice } from 'nft/utils/currency'
import { getTimeDifference, isValidDate } from 'nft/utils/date'
import { putCommas } from 'nft/utils/putCommas'
import { fallbackProvider, getRarityProviderLogo } from 'nft/utils/rarity'
@ -59,14 +60,16 @@ const AddressLink = styled(ExternalLink)`
const formatListingStatus = (status: OrderStatus): string => {
switch (status) {
case OrderStatus.EXECUTED:
case OrderStatus.Executed:
return 'Sold'
case OrderStatus.CANCELLED:
case OrderStatus.Cancelled:
return 'Cancelled'
case OrderStatus.EXPIRED:
case OrderStatus.Expired:
return 'Expired'
case OrderStatus.VALID:
case OrderStatus.Valid:
return 'Add to Bag'
default:
return ''
}
}
@ -93,9 +96,10 @@ export const BuyCell = ({
isMobile,
ethPriceInUSD,
}: BuyCellProps) => {
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const asset = useMemo(
() => buildActivityAsset(event, collectionName, ethPriceInUSD),
[event, collectionName, ethPriceInUSD]
() => buildActivityAsset(event, collectionName, ethPriceInUSD, isNftGraphqlEnabled),
[event, collectionName, ethPriceInUSD, isNftGraphqlEnabled]
)
const isSelected = useMemo(() => {
return itemsInBag.some((item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address)
@ -112,19 +116,19 @@ export const BuyCell = ({
return (
<Column display={{ sm: 'none', lg: 'flex' }} height="full" justifyContent="center" marginX="auto">
{event.eventType === ActivityEventType.Listing && event.orderStatus ? (
{event.eventType === NftActivityType.Listing && event.orderStatus ? (
<Box
as="button"
className={event.orderStatus === OrderStatus.VALID && isSelected ? styles.removeCell : styles.buyCell}
className={event.orderStatus === OrderStatus.Valid && isSelected ? styles.removeCell : styles.buyCell}
onClick={(e: MouseEvent) => {
e.preventDefault()
isSelected ? removeAsset([asset]) : selectAsset([asset])
!isSelected && !cartExpanded && !isMobile && toggleCart()
!isSelected && sendAnalyticsEvent(NFTEventName.NFT_BUY_ADDED, { eventProperties })
}}
disabled={event.orderStatus !== OrderStatus.VALID}
disabled={event.orderStatus !== OrderStatus.Valid}
>
{event.orderStatus === OrderStatus.VALID ? (
{event.orderStatus === OrderStatus.Valid ? (
<>{`${isSelected ? 'Remove' : 'Add to bag'}`}</>
) : (
<>{`${formatListingStatus(event.orderStatus)}`}</>
@ -159,12 +163,12 @@ export const AddressCell = ({ address, desktopLBreakpoint, chainId }: AddressCel
)
}
export const MarketplaceIcon = ({ marketplace }: { marketplace: Markets }) => {
export const MarketplaceIcon = ({ marketplace }: { marketplace: Markets | string }) => {
return (
<Box
as="img"
alt={marketplace}
src={`/nft/svgs/marketplaces/${marketplace}.svg`}
src={`/nft/svgs/marketplaces/${marketplace.toLowerCase()}.svg`}
className={styles.marketplaceIcon}
/>
)
@ -183,8 +187,17 @@ const PriceTooltip = ({ price }: { price: string }) => (
</MouseoverTooltip>
)
export const PriceCell = ({ marketplace, price }: { marketplace?: Markets; price?: string }) => {
const formattedPrice = useMemo(() => (price ? putCommas(formatEthPrice(price))?.toString() : null), [price])
export const PriceCell = ({ marketplace, price }: { marketplace?: Markets | string; price?: string | number }) => {
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const formattedPrice = useMemo(
() =>
price
? isNftGraphqlEnabled
? formatEth(parseFloat(price?.toString()))
: putCommas(formatEthPrice(price.toString()))?.toString()
: null,
[isNftGraphqlEnabled, price]
)
return (
<Row display={{ sm: 'none', md: 'flex' }} gap="8">
@ -203,23 +216,23 @@ export const PriceCell = ({ marketplace, price }: { marketplace?: Markets; price
}
interface EventCellProps {
eventType: ActivityEventType
eventType: NftActivityType
eventTimestamp?: number
eventTransactionHash?: string
eventOnly?: boolean
price?: string
price?: string | number
isMobile?: boolean
}
const renderEventIcon = (eventType: ActivityEventType) => {
const renderEventIcon = (eventType: NftActivityType) => {
switch (eventType) {
case ActivityEventType.Listing:
case NftActivityType.Listing:
return <ActivityListingIcon width={16} height={16} />
case ActivityEventType.Sale:
case NftActivityType.Sale:
return <ActivitySaleIcon width={16} height={16} />
case ActivityEventType.Transfer:
case NftActivityType.Transfer:
return <ActivityTransferIcon width={16} height={16} />
case ActivityEventType.CancelListing:
case NftActivityType.CancelListing:
return <CancelListingIcon width={16} height={16} />
default:
return null
@ -237,12 +250,12 @@ const ExternalLinkIcon = ({ transactionHash }: { transactionHash: string }) => (
</Row>
)
const eventColors = (eventType: ActivityEventType) => {
const eventColors = (eventType: NftActivityType) => {
const activityEvents = {
[ActivityEventType.Listing]: 'gold',
[ActivityEventType.Sale]: 'green',
[ActivityEventType.Transfer]: 'violet',
[ActivityEventType.CancelListing]: 'accentFailure',
[NftActivityType.Listing]: 'gold',
[NftActivityType.Sale]: 'green',
[NftActivityType.Transfer]: 'violet',
[NftActivityType.CancelListing]: 'accentFailure',
}
return activityEvents[eventType] as 'gold' | 'green' | 'violet' | 'accentFailure'
@ -256,16 +269,25 @@ export const EventCell = ({
price,
isMobile,
}: EventCellProps) => {
const formattedPrice = useMemo(() => (price ? putCommas(formatEthPrice(price))?.toString() : null), [price])
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const formattedPrice = useMemo(
() =>
price
? isNftGraphqlEnabled
? formatEth(parseFloat(price?.toString()))
: putCommas(formatEthPrice(price.toString()))?.toString()
: null,
[isNftGraphqlEnabled, price]
)
return (
<Column height="full" justifyContent="center" gap="4">
<Row className={styles.eventDetail} color={eventColors(eventType)}>
{renderEventIcon(eventType)}
{ActivityEventTypeDisplay[eventType]}
</Row>
{eventTimestamp && isValidDate(eventTimestamp) && !isMobile && !eventOnly && (
{eventTimestamp && (isValidDate(eventTimestamp) || isNftGraphqlEnabled) && !isMobile && !eventOnly && (
<Row className={styles.eventTime}>
{getTimeDifference(eventTimestamp.toString())}
{getTimeDifference(eventTimestamp.toString(), isNftGraphqlEnabled)}
{eventTransactionHash && <ExternalLinkIcon transactionHash={eventTransactionHash} />}
</Row>
)}
@ -310,14 +332,18 @@ const NoContentContainer = () => (
)
interface RankingProps {
rarity: TokenRarity
rarity: TokenRarity | Rarity
collectionName: string
rarityVerified: boolean
details?: boolean
}
const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => {
const rarityProviderLogo = getRarityProviderLogo(rarity.source)
const source = (rarity as TokenRarity).source || (rarity as Rarity).primaryProvider
const rank = (rarity as TokenRarity).rank || (rarity as Rarity).providers?.[0].rank
const rarityProviderLogo = getRarityProviderLogo(source)
if (!rank) return null
return (
<Box>
@ -330,7 +356,7 @@ const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => {
<Box width="full" fontSize="14">
{rarityVerified
? `Verified by ${collectionName}`
: `Ranking by ${rarity.source === 'Genie' ? fallbackProvider : rarity.source}`}
: `Ranking by ${source === 'Genie' ? fallbackProvider : source}`}
</Box>
</Row>
}
@ -338,7 +364,7 @@ const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => {
>
<Box className={styles.rarityInfo}>
<Box paddingTop="2" paddingBottom="2" display="flex">
{putCommas(rarity.rank)}
{putCommas(rank)}
</Box>
<Box display="flex" height="16">
@ -357,6 +383,7 @@ const getItemImage = (tokenMetadata?: TokenMetadata): string | undefined => {
export const ItemCell = ({ event, rarityVerified, collectionName, eventTimestamp, isMobile }: ItemCellProps) => {
const [loaded, setLoaded] = useState(false)
const [noContent, setNoContent] = useState(!getItemImage(event.tokenMetadata))
const isNftGraphqlEnabled = useNftGraphqlEnabled()
return (
<Row gap="16" overflow="hidden" whiteSpace="nowrap">
@ -385,7 +412,10 @@ export const ItemCell = ({ event, rarityVerified, collectionName, eventTimestamp
collectionName={collectionName}
/>
)}
{isMobile && eventTimestamp && isValidDate(eventTimestamp) && getTimeDifference(eventTimestamp.toString())}
{isMobile &&
eventTimestamp &&
(isValidDate(eventTimestamp) || isNftGraphqlEnabled) &&
getTimeDifference(eventTimestamp.toString(), isNftGraphqlEnabled)}
</Column>
</Row>
)

@ -1,10 +1,11 @@
import { Trans } from '@lingui/macro'
import { OpacityHoverState, ScrollBarStyles } from 'components/Common'
import { LoadingBubble } from 'components/Tokens/loading'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { EventCell, MarketplaceIcon } from 'nft/components/collection/ActivityCells'
import { ActivityEventResponse } from 'nft/types'
import { ActivityEvent } from 'nft/types'
import { shortenAddress } from 'nft/utils/address'
import { formatEthPrice } from 'nft/utils/currency'
import { formatEth, formatEthPrice } from 'nft/utils/currency'
import { getTimeDifference } from 'nft/utils/date'
import { putCommas } from 'nft/utils/putCommas'
import { ReactNode } from 'react'
@ -147,14 +148,19 @@ export const LoadingAssetActivity = ({ rowCount }: { rowCount: number }) => {
)
}
const AssetActivity = ({ eventsData }: { eventsData: ActivityEventResponse | undefined }) => {
const AssetActivity = ({ events }: { events: ActivityEvent[] | undefined }) => {
const isNftGraphqlEnabled = useNftGraphqlEnabled()
return (
<ActivityTable>
{eventsData?.events &&
eventsData.events.map((event, index) => {
{events &&
events.map((event, index) => {
const { eventTimestamp, eventType, fromAddress, marketplace, price, toAddress, transactionHash } = event
const formattedPrice = price ? putCommas(formatEthPrice(price)).toString() : null
const formattedPrice = price
? isNftGraphqlEnabled
? formatEth(parseFloat(price ?? ''))
: putCommas(formatEthPrice(price)).toString()
: null
if (!eventType) return null
return (
<TR key={index}>
<TD>
@ -189,7 +195,7 @@ const AssetActivity = ({ eventsData }: { eventsData: ActivityEventResponse | und
</Link>
)}
</TD>
<TD>{eventTimestamp && getTimeDifference(eventTimestamp.toString())}</TD>
<TD>{eventTimestamp && getTimeDifference(eventTimestamp.toString(), isNftGraphqlEnabled)}</TD>
</TR>
)
})}

@ -1,6 +1,9 @@
import { OpacityHoverState, ScrollBarStyles } from 'components/Common'
import Resource from 'components/Tokens/TokenDetails/Resource'
import { MouseoverTooltip } from 'components/Tooltip/index'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { NftActivityType } from 'graphql/data/__generated__/types-and-hooks'
import { useNftActivity } from 'graphql/data/nft/NftActivity'
import { Box } from 'nft/components/Box'
import { reduceFilters } from 'nft/components/collection/Activity'
import { LoadingSparkle } from 'nft/components/common/Loading/LoadingSparkle'
@ -10,7 +13,7 @@ import { themeVars, vars } from 'nft/css/sprinkles.css'
import { ActivityFetcher } from 'nft/queries/genie/ActivityFetcher'
import { ActivityEventResponse, ActivityEventType, CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { shortenAddress } from 'nft/utils/address'
import { formatEthPrice } from 'nft/utils/currency'
import { formatEth, formatEthPrice } from 'nft/utils/currency'
import { isAudio } from 'nft/utils/isAudio'
import { isVideo } from 'nft/utils/isVideo'
import { putCommas } from 'nft/utils/putCommas'
@ -244,6 +247,7 @@ interface AssetDetailsProps {
}
export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const [dominantColor] = useState<[number, number, number]>([0, 0, 0])
const { rarityProvider } = useMemo(
@ -299,10 +303,26 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
refetchOnMount: false,
}
)
const { nftActivity: gqlPriceData } = useNftActivity(
{
activityTypes: [NftActivityType.Sale],
address: contractAddress,
tokenId: token_id,
},
1,
'no-cache'
)
const lastSalePrice = priceData?.events[0]?.price ?? null
const formattedEthprice = formatEthPrice(lastSalePrice ?? '') || 0
const formattedPrice = lastSalePrice ? putCommas(formattedEthprice).toString() : null
// TODO simplify typecasting when removing graphql flag
const lastSalePrice = isNftGraphqlEnabled ? gqlPriceData?.[0]?.price : priceData?.events[0]?.price
const formattedEthprice = isNftGraphqlEnabled
? formatEth(parseFloat(lastSalePrice ?? ''))
: formatEthPrice(lastSalePrice) || 0
const formattedPrice = isNftGraphqlEnabled
? formattedEthprice
: lastSalePrice
? putCommas(parseFloat(formattedEthprice.toString())).toString()
: null
const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState)
const Filter = useCallback(
@ -365,13 +385,48 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
}
)
const rarity = asset?.rarity?.providers?.length ? asset?.rarity?.providers?.[0] : undefined
const {
nftActivity: gqlEventsData,
hasNext,
loadMore,
loading,
error,
} = useNftActivity(
{
activityTypes: Object.keys(activeFilters)
.map((key) => key as NftActivityType)
.filter((key) => activeFilters[key]),
address: contractAddress,
tokenId: token_id,
},
25
)
const { events, gatedHasNext, gatedLoadMore, gatedLoading, gatedSuccess } = useMemo(() => {
return {
events: isNftGraphqlEnabled ? gqlEventsData : eventsData?.pages.map((page) => page.events).flat(),
gatedHasNext: isNftGraphqlEnabled ? hasNext : hasNextPage,
gatedLoadMore: isNftGraphqlEnabled ? loadMore : fetchNextPage,
gatedLoading: isNftGraphqlEnabled ? loading : isActivityLoading,
gatedSuccess: isNftGraphqlEnabled ? !error : isSuccess,
}
}, [
error,
eventsData?.pages,
fetchNextPage,
gqlEventsData,
hasNext,
hasNextPage,
isActivityLoading,
isNftGraphqlEnabled,
isSuccess,
loadMore,
loading,
])
const rarity = asset?.rarity?.providers?.[0]
const [showHolder, setShowHolder] = useState(false)
const rarityProviderLogo = getRarityProviderLogo(rarity?.provider)
const events = useMemo(
() => (isSuccess ? eventsData?.pages.map((page) => page.events).flat() : null),
[isSuccess, eventsData]
)
return (
<Column>
@ -433,11 +488,12 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
<Filter eventType={ActivityEventType.Transfer} />
<Filter eventType={ActivityEventType.CancelListing} />
</ActivitySelectContainer>
{isActivityLoading && <LoadingAssetActivity rowCount={10} />}
{events && events.length > 0 ? (
{gatedLoading ? (
<LoadingAssetActivity rowCount={10} />
) : events && events.length > 0 ? (
<InfiniteScroll
next={fetchNextPage}
hasMore={!!hasNextPage}
next={gatedLoadMore}
hasMore={!!gatedHasNext}
loader={
isFetchingNextPage && (
<Center>
@ -448,11 +504,11 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
dataLength={events?.length ?? 0}
scrollableTarget="activityContainer"
>
<AssetActivity eventsData={{ events }} />
<AssetActivity events={events} />
</InfiniteScroll>
) : (
<>
{!isActivityLoading && (
{gatedSuccess && events && (
<EmptyActivitiesContainer>
<div>No activities yet</div>
<Link to={`/nfts/collection/${asset.address}`}>View collection items</Link>{' '}

@ -1,4 +1,6 @@
import { Markets, TokenType } from '../common'
import { NftActivityType, NftStandard, OrderStatus } from 'graphql/data/__generated__/types-and-hooks'
import { Markets, Rarity, TokenType } from '../common'
export interface AssetPayload {
filters: {
traits?: Record<string, string[]>
@ -57,13 +59,6 @@ export enum ActivityEventTypeDisplay {
'CANCEL_LISTING' = 'Cancellation',
}
export enum OrderStatus {
VALID = 'VALID',
EXECUTED = 'EXECUTED',
CANCELLED = 'CANCELLED',
EXPIRED = 'EXPIRED',
}
export interface ActivityFilter {
collectionAddress?: string
eventTypes?: ActivityEventType[]
@ -83,31 +78,29 @@ export interface TokenRarity {
}
export interface TokenMetadata {
name: string
imageUrl: string
smallImageUrl: string
metadataUrl: string
rarity: TokenRarity
suspiciousFlag: boolean
suspiciousFlaggedBy: string
standard: TokenType
name?: string
imageUrl?: string
smallImageUrl?: string
metadataUrl?: string
rarity?: TokenRarity | Rarity
suspiciousFlag?: boolean
standard?: TokenType | NftStandard
}
// TODO when deprecating activity query, remove all outdated types (former in optional fields)
export interface ActivityEvent {
collectionAddress: string
collectionAddress?: string
tokenId?: string
tokenMetadata?: TokenMetadata
eventType: ActivityEventType
marketplace?: Markets
fromAddress: string
eventType?: NftActivityType
marketplace?: Markets | string
fromAddress?: string
toAddress?: string
transactionHash?: string
orderHash?: string
orderStatus?: OrderStatus
price?: string
symbol?: string
quantity?: number
auctionType?: string
url?: string
eventTimestamp?: number
}

@ -9,7 +9,7 @@ import { isAddress } from '@ethersproject/address'
*/
export function shortenAddress(address: string, charsStart = 4, charsEnd?: number): string {
const parsed = isAddress(address)
if (!parsed) throw Error(`Invalid 'address' parameter '${address}'.`)
if (!parsed) return ''
return `${address.substring(0, charsStart + 2)}...${address.substring(42 - (charsEnd || charsStart))}`
}

@ -1,16 +1,28 @@
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { parseEther } from 'ethers/lib/utils'
import { ActivityEvent, GenieAsset } from 'nft/types'
export const buildActivityAsset = (event: ActivityEvent, collectionName: string, ethPriceInUSD: number): GenieAsset => {
import { formatEth } from './currency'
export const buildActivityAsset = (
event: ActivityEvent,
collectionName: string,
ethPriceInUSD: number,
isNftGraphqlEnabled: boolean
): GenieAsset => {
const assetUsdPrice = event.price
? formatEther(
BigNumber.from(event.price)
.mul(BigNumber.from(Math.trunc(ethPriceInUSD * 100)))
.div(100)
)
? isNftGraphqlEnabled
? formatEth(parseFloat(event.price) * ethPriceInUSD)
: formatEther(
BigNumber.from(event.price)
.mul(BigNumber.from(Math.trunc(ethPriceInUSD * 100)))
.div(100)
)
: '0'
const weiPrice = isNftGraphqlEnabled ? (event.price ? parseEther(event.price) : '') : event.price
return {
address: event.collectionAddress,
collectionName,
@ -23,8 +35,8 @@ export const buildActivityAsset = (event: ActivityEvent, collectionName: string,
collectionSymbol: event.symbol,
priceInfo: {
USDPrice: assetUsdPrice,
ETHPrice: event.price,
basePrice: event.price,
ETHPrice: weiPrice,
basePrice: weiPrice,
baseAsset: 'ETH',
},
tokenType: event.tokenMetadata?.standard,

@ -3,8 +3,8 @@ export const isValidDate = (date: number): boolean => {
return isNaN(d) ? false : true
}
export const getTimeDifference = (eventTimestamp: string) => {
const date = new Date(eventTimestamp).getTime()
export const getTimeDifference = (eventTimestamp: string, isNftGraphqlEnabled: boolean) => {
const date = isNftGraphqlEnabled ? parseFloat(eventTimestamp) : new Date(eventTimestamp).getTime()
const diff = new Date().getTime() - date
const days = Math.floor(diff / (1000 * 60 * 60 * 24))