Skip to content

Commit

Permalink
[PAY-2648] Improve state of collectibles and their collections (#7979)
Browse files Browse the repository at this point in the history
Co-authored-by: Saliou Diallo <saliou@audius.co>
  • Loading branch information
sddioulde and Saliou Diallo committed Apr 2, 2024
1 parent d0381e4 commit bfe63b9
Show file tree
Hide file tree
Showing 14 changed files with 127 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ export class EthereumCollectiblesProvider implements CollectiblesProvider {
if (ownedCollectibleKeySet.has(id)) {
collectiblesMap[id] = {
...collectiblesMap[id],
dateLastTransferred: dayjs(event.event_timestamp).toString()
dateLastTransferred: dayjs(
event.event_timestamp * 1000
).toString()
}
} else {
ownedCollectibleKeySet.add(id)
Expand Down Expand Up @@ -256,7 +258,9 @@ export class EthereumCollectiblesProvider implements CollectiblesProvider {
} else {
collectiblesMap[id] = {
...collectiblesMap[id],
dateLastTransferred: dayjs(event.event_timestamp).toString()
dateLastTransferred: dayjs(
event.event_timestamp * 1000
).toString()
}
}
} else if (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Connection, PublicKey } from '@solana/web3.js'

import { Collectible, CollectibleState } from '~/models'
import { allSettled } from '~/utils'
import { Nullable, allSettled } from '~/utils'

import { HeliusClient } from '../helius'
import { HeliusNFT } from '../helius/types'

import { CollectiblesProvider } from './CollectiblesProvider'
import { solanaNFTToCollectible } from './solCollectibleHelpers'
import { SolanaNFTType } from './types'
import {
isHeliusNFTValid,
solanaNFTToCollectible
} from './solCollectibleHelpers'
import { Blocklist, SolanaNFTType } from './types'

const BLOCKLIST_URL =
'https://raw.githubusercontent.com/solflare-wallet/blocklist-automation/master/dist/blocklist.json'

type SolanaCollectiblesProviderCtorArgs = {
heliusClient: HeliusClient
Expand All @@ -18,8 +24,9 @@ type SolanaCollectiblesProviderCtorArgs = {

export class SolanaCollectiblesProvider implements CollectiblesProvider {
private readonly heliusClient: HeliusClient
private metadataProgramIdPublicKey: PublicKey
private connection: Connection | null = null
private readonly metadataProgramIdPublicKey: PublicKey
private readonly connection: Nullable<Connection> = null
private blocklist: Nullable<Blocklist> = null

constructor({
heliusClient,
Expand All @@ -39,6 +46,15 @@ export class SolanaCollectiblesProvider implements CollectiblesProvider {
}

async getCollectibles(wallets: string[]): Promise<CollectibleState> {
if (!this.blocklist) {
try {
const blocklistResponse = await fetch(BLOCKLIST_URL)
this.blocklist = await blocklistResponse.json()
} catch (e) {
console.error('Could not fetch Solana nft blocklist', e)
}
}

const results = await allSettled(
wallets.map((wallet) => this.heliusClient.getNFTsForWallet(wallet))
)
Expand All @@ -62,6 +78,12 @@ export class SolanaCollectiblesProvider implements CollectiblesProvider {
}
)
.map(({ result, wallet }) => {
const blocklist = this.blocklist
if (blocklist) {
return result.value
.filter((nft) => isHeliusNFTValid(nft, blocklist))
.map((nft) => ({ ...nft, wallet }))
}
return result.value.map((nft) => ({ ...nft, wallet }))
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ export const transferEventToCollectible = async (
return {
...collectible,
isOwned,
dateLastTransferred: dayjs(event_timestamp).toString()
dateLastTransferred: dayjs(event_timestamp * 1000).toString()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Nullable } from '../../utils/typeUtils'
import { HeliusNFT } from '../helius'

import {
Blocklist,
MetaplexNFT,
MetaplexNFTPropertiesFile,
SolanaNFT,
Expand Down Expand Up @@ -467,6 +468,71 @@ const heliusNFTToCollectible = async (
return collectible
}

const audiusBlocklistUrls = [
'.pro',
'.site',
'.click',
'.fun',
'sol-drift.com',
'myrovoucher.com',
'magiceden.club'
]
const audiusBlocklistNames = [
'00jup',
'airdrop',
'voucher',
...audiusBlocklistUrls
]
export const isHeliusNFTValid = (nft: HeliusNFT, blocklist: Blocklist) => {
const {
blocklist: urlBlocklist,
nftBlocklist,
stringFilters: { nameContains, symbolContains }
} = blocklist
const {
grouping,
content: {
metadata: { name, symbol },
links: { external_url: externalUrl }
}
} = nft
const urlBlocklistExtended = [...urlBlocklist, ...audiusBlocklistUrls]
const isExternalUrlBlocked = urlBlocklistExtended.some((item) =>
externalUrl?.toLowerCase().includes(item.toLowerCase())
)
if (isExternalUrlBlocked) {
return false
}
const isNftIdBlocked = nftBlocklist.includes(nft.id)
if (isNftIdBlocked) {
return false
}
const nameContainsExtended = [...nameContains, ...audiusBlocklistNames]
const isNameBlocked = nameContainsExtended.some((item) =>
name?.toLowerCase().includes(item.toLowerCase())
)
if (isNameBlocked) {
return false
}
const isCollectionNameBlocked = grouping.some((group) =>
nameContainsExtended.some((item) =>
group.collection_metadata?.name
?.toLowerCase()
.includes(item.toLowerCase())
)
)
if (isCollectionNameBlocked) {
return false
}
const isSymbolBlocked = symbolContains.some((item) =>
symbol?.toLowerCase().includes(item.toLowerCase())
)
if (isSymbolBlocked) {
return false
}
return true
}

export const solanaNFTToCollectible = async (
nft: SolanaNFT,
wallet: string,
Expand Down
10 changes: 10 additions & 0 deletions packages/common/src/services/collectibles-provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,13 @@ export type StarAtlasNFT = {
}

export type SolanaNFT = HeliusNFT | MetaplexNFT | StarAtlasNFT

export type Blocklist = {
blocklist: string[] // list of urls
nftBlocklist: string[] // list of nft ids
stringFilters: {
nameContains: string[]
symbolContains: string[]
}
contentHash: string
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
import { useCallback } from 'react'

import { useStreamConditionsEntity } from '@audius/common/hooks'
import { useFeatureFlag, useStreamConditionsEntity } from '@audius/common/hooks'
import {
FollowSource,
ModalSource,
Expand All @@ -12,6 +12,7 @@ import {
isContentUSDCPurchaseGated
} from '@audius/common/models'
import type { ID, AccessConditions, User } from '@audius/common/models'
import { FeatureFlags } from '@audius/common/services'
import {
usersSocialActions,
tippingActions,
Expand Down
3 changes: 0 additions & 3 deletions packages/mobile/src/screens/profile-screen/constants.ts

This file was deleted.

10 changes: 0 additions & 10 deletions packages/mobile/src/screens/profile-screen/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { useSelectTierInfo } from '@audius/common/hooks'
import type { CommonState } from '@audius/common/store'
import { badgeTiers } from '@audius/common/store'
import { useSelector } from 'react-redux'

import { MIN_COLLECTIBLES_TIER } from './constants'
import { getIsOwner, useSelectProfile } from './selectors'

/**
Expand All @@ -20,7 +17,6 @@ import { getIsOwner, useSelectProfile } from './selectors'
export const useShouldShowCollectiblesTab = () => {
const {
handle,
user_id,
has_collectibles,
collectibleList,
solanaCollectibleList,
Expand All @@ -34,12 +30,6 @@ export const useShouldShowCollectiblesTab = () => {
'collectibles'
])
const isOwner = useSelector((state: CommonState) => getIsOwner(state, handle))
const { tierNumber } = useSelectTierInfo(user_id)

const hasCollectiblesTierRequirement =
tierNumber >= badgeTiers.findIndex((t) => t.tier === MIN_COLLECTIBLES_TIER)

if (!hasCollectiblesTierRequirement) return false

const hasCollectibles =
collectibleList?.length || solanaCollectibleList?.length
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import {
useState
} from 'react'

import { useSelectTierInfo } from '@audius/common/hooks'
import { Chain, CollectibleMediaType, Collectible } from '@audius/common/models'
import {
accountSelectors,
badgeTiers,
collectibleDetailsUISelectors,
collectibleDetailsUIActions
} from '@audius/common/store'
Expand Down Expand Up @@ -44,7 +41,6 @@ import { ToastContext } from 'components/toast/ToastContext'
import Tooltip from 'components/tooltip/Tooltip'
import { ComponentPlacement, MountPlacement } from 'components/types'
import { useIsMobile } from 'hooks/useIsMobile'
import { MIN_COLLECTIBLES_TIER } from 'pages/profile-page/ProfilePageProvider'
import { copyToClipboard, getCopyableLink } from 'utils/clipboardUtil'
import zIndex from 'utils/zIndex'

Expand All @@ -59,7 +55,6 @@ const Collectible3D = lazy(() =>

const { setCollectible } = collectibleDetailsUIActions
const { getCollectibleDetails, getCollectible } = collectibleDetailsUISelectors
const getAccountUser = accountSelectors.getAccountUser

type CollectibleMediaProps = {
collectible: Collectible
Expand Down Expand Up @@ -154,13 +149,6 @@ const CollectibleDetailsModal = ({
const [isPicConfirmModalOpen, setIsPicConfirmaModalOpen] =
useState<boolean>(false)

const accountUser = useSelector(getAccountUser)
const userId = accountUser?.user_id ?? 0
const { tierNumber } = useSelectTierInfo(userId)

const isCollectibleOptionEnabled =
tierNumber >= badgeTiers.findIndex((t) => t.tier === MIN_COLLECTIBLES_TIER)

const handleClose = useCallback(() => {
dispatch(setCollectible({ collectible: null }))
setIsModalOpen(false)
Expand Down Expand Up @@ -317,8 +305,7 @@ const CollectibleDetailsModal = ({
Embed
</Button>

{isCollectibleOptionEnabled &&
isUserOnTheirProfile &&
{isUserOnTheirProfile &&
collectible.mediaType === CollectibleMediaType.IMAGE && (
<Button
variant='secondary'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useState, useCallback, useRef, useContext } from 'react'

import { useSelectTierInfo } from '@audius/common/hooks'
import { RandomImage } from '@audius/common/services'
import { accountSelectors, badgeTiers } from '@audius/common/store'
import { accountSelectors } from '@audius/common/store'
import { removeNullable } from '@audius/common/utils'
import { Button, Popup, SegmentedControl, IconSearch } from '@audius/harmony'
import cn from 'classnames'
Expand All @@ -13,7 +12,6 @@ import { useWindowSize } from 'react-use'
import { Dropzone } from 'components/upload/Dropzone'
import InvalidFileType from 'components/upload/InvalidFileType'
import { MainContentContext } from 'pages/MainContentContext'
import { MIN_COLLECTIBLES_TIER } from 'pages/profile-page/ProfilePageProvider'
import zIndex from 'utils/zIndex'

import styles from './ImageSelectionPopup.module.css'
Expand Down Expand Up @@ -233,16 +231,8 @@ const ImageSelectionPopup = ({
const { mainContentRef } = useContext(MainContentContext)
const [page, setPage] = useState(messages.uploadYourOwn)
const windowSize = useWindowSize()
const {
collectibles,
collectibleList,
solanaCollectibleList,
user_id: userId
} = useSelector(getAccountUser)

const { tierNumber } = useSelectTierInfo(userId ?? 0)
const isCollectibleOptionEnabled =
tierNumber >= badgeTiers.findIndex((t) => t.tier === MIN_COLLECTIBLES_TIER)
const { collectibles, collectibleList, solanaCollectibleList } =
useSelector(getAccountUser)

const allCollectibles = [
...(collectibleList || []),
Expand Down Expand Up @@ -277,7 +267,7 @@ const ImageSelectionPopup = ({
text: messages.findArtwork
}
]
if (isCollectibleOptionEnabled && visibleCollectibles.length) {
if (visibleCollectibles.length) {
tabSliderOptions.push({
key: messages.yourCollectibles,
text: messages.yourCollectibles
Expand Down
11 changes: 1 addition & 10 deletions packages/web/src/pages/audio-rewards-page/Tiers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ const messages = {
learnMore: 'Learn more',
launchDiscord: 'Launch the VIP Discord',
tierLevel: (amount: string) => `${Number(amount).toLocaleString()}+ $AUDIO`,
matrixMode: 'Matrix Mode',
collectibles: 'NFT Collectibles'
matrixMode: 'Matrix Mode'
}

type AudioTiers = Exclude<BadgeTier, 'none'>
Expand Down Expand Up @@ -175,14 +174,6 @@ export const Tier = ({
<i className='emoji large white-heavy-check-mark' />
{messages.badgeRole(tier)}
</span>
{(tier === 'silver' ||
tier === 'gold' ||
tier === 'platinum') && (
<span>
<i className='emoji large framed-picture' />
{messages.collectibles}
</span>
)}
{(tier === 'gold' || tier === 'platinum') && (
<span>
<i className='emoji large rabbit' />
Expand Down
3 changes: 0 additions & 3 deletions packages/web/src/pages/profile-page/ProfilePageProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
FollowSource,
CreatePlaylistSource,
Status,
BadgeTier,
ID,
UID
} from '@audius/common/models'
Expand Down Expand Up @@ -132,8 +131,6 @@ type ProfilePageState = {
showUnblockUserConfirmationModal: boolean
}

export const MIN_COLLECTIBLES_TIER: BadgeTier = 'silver'

class ProfilePage extends PureComponent<ProfilePageProps, ProfilePageState> {
static defaultProps = {}
static contextType = SsrContext
Expand Down
Loading

0 comments on commit bfe63b9

Please sign in to comment.