Skip to content

Commit

Permalink
feat: persist NFT collections (#2452)
Browse files Browse the repository at this point in the history
* feat: persist nft collections

* refactor: remove derived and subscribe

* feat: updates collections where nfts are added

* fix: collections display

* chore: adds Mark suggestions

---------

Co-authored-by: Mark Nardi <[email protected]>
  • Loading branch information
jeeanribeiro and MarkNerdi authored May 8, 2024
1 parent c6a374e commit f44ba33
Show file tree
Hide file tree
Showing 28 changed files with 192 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { TokenTrackingStatus } from '@core/token'
import { selectedAccount } from '@core/account/stores'
import { addOrUpdateNftForAccount } from '@core/nfts/stores'
import { persistAndUpdateCollections } from '@core/nfts/actions'
let busy = false
Expand Down Expand Up @@ -43,6 +44,7 @@
const l2Address = getAddressFromAccountForNetwork(account, networkId)
const nft = buildNftFromPersistedErc721Nft(persistedNft, l2Address)
addOrUpdateNftForAccount(account.index, nft)
await persistAndUpdateCollections(account.index, [nft])
void addNftsToDownloadQueue([nft])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { handleError } from '@core/error/handlers'
import { EvmNetworkId, NetworkNamespace, getEvmNetwork } from '@core/network'
import { buildNftFromPersistedErc721Nft } from '@core/nfts'
import { addNftsToDownloadQueue } from '@core/nfts/actions'
import { addNftsToDownloadQueue, persistAndUpdateCollections } from '@core/nfts/actions'
import { persistErc721Nft } from '@core/nfts/actions/persistErc721Nft'
import { addOrUpdateNftForAccount, ownedNfts } from '@core/nfts/stores'
import { TideApi } from '@core/tide/apis'
Expand Down Expand Up @@ -80,6 +80,7 @@
const nft = buildNftFromPersistedErc721Nft(persistedNft, accountAddress)
void addNftsToDownloadQueue([nft])
addOrUpdateNftForAccount(index, nft)
await persistAndUpdateCollections(index, [nft])
}
} catch (_) {
// Switching account too swiftly results in an error from persistErc721Nft.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import { Collection, Nft } from '@core/nfts/interfaces'
import { NftStandard } from '@core/nfts/enums'
import {
activeProfileCollectionsPerAccount,
activeProfileNftsPerAccount,
getNftByIdForAccount,
selectedAccountCollections,
selectedCollectionId,
selectedNftId,
} from '@core/nfts/stores'
Expand All @@ -18,7 +18,9 @@
let nft: Nft | undefined
let collection: Collection | undefined
$: $activeProfileNftsPerAccount, (nft = getNftByIdForAccount($selectedAccountIndex, $selectedNftId))
$: collection = $selectedCollectionId ? $selectedAccountCollections[$selectedCollectionId] : undefined
$: collection = $selectedCollectionId
? $activeProfileCollectionsPerAccount[$selectedAccountIndex][$selectedCollectionId]
: undefined
$: returnIfNftWasSent($activeProfileNftsPerAccount[$selectedAccountIndex], $time)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
import features from '@features/features'
import { SearchInput } from '@ui'
import { CollectiblesTabs, CollectionsGallery } from '../components'
import { collectionsSearchTerm, selectedAccountCollections } from '@core/nfts/stores'
import { activeProfileCollectionsPerAccount, collectionsSearchTerm } from '@core/nfts/stores'
import { Collections, isVisibleCollection } from '@core/nfts'
import { selectedAccountIndex } from '@core/account/stores'
function onReceiveClick(): void {
openPopup({
Expand All @@ -17,23 +18,28 @@
let queriedCollections: Collections = {}
$: $collectionsSearchTerm,
(queriedCollections = Object.fromEntries(
Object.entries($selectedAccountCollections)
.filter(([, collection]) => isVisibleCollection(collection))
.sort(([, collection1], [, collection2]) =>
collection1?.name.toLowerCase().localeCompare(collection2?.name.toLowerCase())
)
))
(queriedCollections = $activeProfileCollectionsPerAccount[$selectedAccountIndex]
? Object.fromEntries(
Object.entries($activeProfileCollectionsPerAccount[$selectedAccountIndex])
.filter(([, collection]) => isVisibleCollection(collection))
.sort(([, collection1], [, collection2]) =>
collection1?.name.toLowerCase().localeCompare(collection2?.name.toLowerCase())
)
)
: {})
$: hasCollections = Object.keys($selectedAccountCollections).length > 0
$: selectedAccountCollectionsLength = $activeProfileCollectionsPerAccount[$selectedAccountIndex]
? Object.keys($activeProfileCollectionsPerAccount[$selectedAccountIndex]).length
: 0
$: hasCollections = selectedAccountCollectionsLength > 0
</script>

<collections-gallery-view class="flex flex-col w-full h-full gap-4">
<header class="flex flex-row items-center justify-between">
<div class="flex flex-row text-left gap-2 items-center flex-1">
<Text type="h6">{localize('views.collectibles.collectionsGallery.title')}</Text>
<Pill color="neutral">
<Text textColor="secondary">{String(Object.keys($selectedAccountCollections).length ?? '')}</Text>
<Text textColor="secondary">{String(selectedAccountCollectionsLength ?? '')}</Text>
</Pill>
</div>
<CollectiblesTabs />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Address, IssuerFeature, NftOutput, FeatureType } from '@iota/sdk/out/types'
import { IssuerFeature, NftOutput, FeatureType } from '@iota/sdk/out/types'

export function getIssuerFromNftOutput(output: NftOutput): Address {
export function getIssuerFromNftOutput(output: NftOutput): { type: number; nftId?: string; aliasId?: string } {
const metadata = output.immutableFeatures?.find((feature) => feature.type === FeatureType.Issuer) as IssuerFeature
return metadata?.address
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IAccountState } from '@core/account'
import { buildNftFromNftOutput } from '@core/nfts/actions'
import { buildNftFromNftOutput, persistAndUpdateCollections } from '@core/nfts/actions'
import { IWrappedOutput } from '@core/wallet'
import { NftOutput, OutputType } from '@iota/sdk/out/types'
import { ActivityAction, ActivityDirection } from '../../enums'
Expand Down Expand Up @@ -58,6 +58,7 @@ export async function generateActivitiesFromBasicOutputs(
)
const nft = buildNftFromNftOutput(wrappedInput, networkId, account.depositAddress, false)
addOrUpdateNftForAccount(account.index, nft)
await persistAndUpdateCollections(account.index, [nft])

burnedNftInputs.splice(burnedNftInputIndex, 1)
} else if (isSelfTransaction && burnedNativeToken) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ContractType } from '@core/layer-2/enums'
import { getSmartContractHexName, evmAddressToAgentId, getAgentBalanceParameters } from '@core/layer-2/helpers'
import { IscChain } from '@core/network'
import { isIrc27Nft, getNftsFromNftIds, Nft } from '@core/nfts'
import { addNftsToDownloadQueue } from '@core/nfts/actions'
import { addNftsToDownloadQueue, persistAndUpdateCollections } from '@core/nfts/actions'
import { addOrUpdateNftsForAccount, selectedAccountNfts, updateNftsForAccount } from '@core/nfts/stores'
import { BASE_TOKEN_ID, ITokenBalance } from '@core/token'
import { getOrRequestTokenFromPersistedTokens } from '@core/token/actions'
Expand Down Expand Up @@ -82,6 +82,7 @@ async function fetchL2Irc27Nfts(

const nfts = await getNftsFromNftIds(newNftIds, networkId)
addOrUpdateNftsForAccount(account.index, nfts)
await persistAndUpdateCollections(account.index, nfts)

const unspendableNfts = nftsForChain
.filter((nft) => !nftIds.some((nftId) => nft.id === nftId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ export function buildNftFromNftOutput(

const isScam = persistedNft?.isScam ?? (parsedMetadata ? isScamIrc27Nft(parsedMetadata) : false)
const expirationTime = getExpirationDateFromOutput(nftOutput)?.getTime()
const collectionId = issuer?.nftId ?? issuer?.aliasId ?? ''

return {
standard: NftStandard.Irc27,
type: parsedMetadata?.type ?? MimeType.TextPlain,
id,
collectionId,
nftAddress: address,
name: parsedMetadata?.name ?? DEFAULT_NFT_NAME,
description: parsedMetadata?.description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TokenTrackingStatus } from '@core/token'
import { IBlockscoutAsset } from '@auxiliary/blockscout/interfaces'
import { BlockscoutApi } from '@auxiliary/blockscout/api'
import { addOrUpdateNftForAccount } from '../stores'
import { persistAndUpdateCollections } from './persistAndUpdateCollections'

export async function checkForUntrackedNfts(account: IAccountState): Promise<void> {
if (!features?.collectibles?.erc721?.enabled) {
Expand Down Expand Up @@ -70,6 +71,7 @@ async function persistNftsFromExplorerAsset(

const nft = buildNftFromPersistedErc721Nft(persistedNft, evmAddress)
addOrUpdateNftForAccount(account.index, nft)
await persistAndUpdateCollections(account.index, [nft])
return nft
} catch (err) {
// If we don't have the tokenId we cannot persist the NFT. ERC-721 contracts should implement
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/lib/core/nfts/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './addNftsToDownloadQueue'
export * from './buildNftFromNftOutput'
export * from './checkForUntrackedNfts'
export * from './downloadNextNftInQueue'
export * from './persistAndUpdateCollections'
export * from './getPersistedErc721NftsForNetwork'
export * from './interruptNftDownloadAfterTimeout'
export * from './isNftPersisted'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Nft } from '../interfaces'
import { addNftsToDownloadQueue } from './addNftsToDownloadQueue'
import { buildNftFromNftOutput } from './buildNftFromNftOutput'
import { setNftsForAccount } from '../stores'
import { persistAndUpdateCollections } from './persistAndUpdateCollections'

export async function loadNftsForActiveProfile(): Promise<void> {
let nftsToDownload: Nft[] = []
Expand Down Expand Up @@ -61,6 +62,7 @@ export async function loadNftsForAccount(account: IAccountState): Promise<Nft[]>
}
}
setNftsForAccount(account.index, accountNfts)
await persistAndUpdateCollections(account.index, accountNfts)

return accountNfts
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { get } from 'svelte/store'
import { addCollectionsToPersistedCollections, addNftsToCollection, persistedCollections } from '../stores'
import { Nft } from '../interfaces'
import { buildPersistedCollectionFromNft } from '../utils'
import { Collections } from '../types'

export async function persistAndUpdateCollections(accountIndex: number, nfts: Nft[]): Promise<void> {
const _persistedCollections = get(persistedCollections)

const collections: Collections = {}
for (const nft of nfts) {
if (!nft.collectionId) {
continue
}
if (!_persistedCollections[nft.collectionId] && !collections[nft.collectionId]) {
const collection = await buildPersistedCollectionFromNft(nft)
if (collection) {
collections[nft.collectionId] = { ...collection, nfts: [] }
}
}
}
addCollectionsToPersistedCollections(Object.values(collections))
addNftsToCollection(accountIndex, nfts)
}
1 change: 1 addition & 0 deletions packages/shared/src/lib/core/nfts/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './nft-filter.interface'
export * from './nft-metadata.interface'
export * from './nft.interface'
export * from './nft-attribute.interface'
export * from './persisted-collection.interface'
export * from './persisted-nft.interface'
export * from './persisted-nfts.interface'
export * from './soonaverse-attribute.interface'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface IErc721Nft extends IBaseNft {

interface IBaseNft {
id: string
collectionId?: string
type: MimeType
networkId: NetworkId
name: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IErc721ContractMetadata } from './erc721-contract-metadata.interface'
import { IIrc27Metadata } from './nft-metadata.interface'

interface IBaseCollection {
id: string
}

export type PersistedCollection = IPersistedIrc27Collection | IPersistedErc721Collection

export interface IPersistedIrc27Collection extends IIrc27Metadata, IBaseCollection {}

export interface IPersistedErc721Collection extends IErc721ContractMetadata, IBaseCollection {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Writable, get, writable } from 'svelte/store'
import { persistedCollections } from '.'
import { Collections } from '../types'
import { Nft } from '../interfaces'
import { NftStandard } from '../enums'

export const collectionsSearchTerm = writable('')

export const activeProfileCollectionsPerAccount: Writable<{
[accountIndex: number]: Collections
}> = writable({})

export function addNftsToCollection(accountIndex: number, nfts: Nft[]): void {
if (!nfts.length) return

const $persistedCollections = get(persistedCollections)
activeProfileCollectionsPerAccount.update((state) => {
for (const nft of nfts) {
if (!nft.collectionId || !$persistedCollections[nft.collectionId]) {
continue
}

if (!state[accountIndex]) {
state[accountIndex] = {}
}

let collection = state[accountIndex][nft.collectionId]
if (!collection) {
collection = { ...$persistedCollections[nft.collectionId], nfts: [] }
}

if (collection.nfts.some((_nft) => _nft.id === nft.id) || !nft.isSpendable) {
continue
}

if (collection.standard === NftStandard.Irc27 && nft.standard === NftStandard.Irc27) {
collection.nfts?.push(nft)
} else if (collection.standard === NftStandard.Erc721 && nft.standard === NftStandard.Erc721) {
collection.nfts?.push(nft)
}
state[accountIndex][nft.collectionId] = collection
}

return state
})
}

export function clearActiveProfileCollectionsPerAccount(): void {
activeProfileCollectionsPerAccount.set({})
}
3 changes: 2 additions & 1 deletion packages/shared/src/lib/core/nfts/stores/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export * from './active-profile-collections-per-account.store'
export * from './active-profile-nfts-per-account.store'
export * from './downloading-nft.store'
export * from './nft-download-queue.store'
export * from './nft-filter.store'
export * from './selected-account-collections.store'
export * from './persisted-collections.store'
export * from './persisted-nfts.store'
export * from './selected-account-nfts.store'
export * from './selected-collectibles-tabs.store'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { persistent } from '@core/utils/store'
import { Collection } from '../interfaces'
import { PersistedCollections } from '../types'

export const persistedCollections = persistent<PersistedCollections>('persistedCollections', {})

export function addCollectionsToPersistedCollections(collections: Collection[]): void {
persistedCollections.update((state) => {
for (const collection of collections) {
if (state[collection.id]) {
continue
}

state[collection.id] = collection
}
return state
})
}

export function addCollectionToPersistedCollections(collection: Collection): void {
addCollectionsToPersistedCollections([collection])
}
Loading

0 comments on commit f44ba33

Please sign in to comment.