diff --git a/packages/desktop/features/network.features.ts b/packages/desktop/features/network.features.ts index 7cf79283c1..2d49c92cb9 100644 --- a/packages/desktop/features/network.features.ts +++ b/packages/desktop/features/network.features.ts @@ -12,7 +12,7 @@ const networkFeatures: INetworkFeatures = { }, }, evmNetworks: { - enabled: true, + enabled: false, }, } diff --git a/packages/desktop/views/dashboard/collectibles/views/CollectionsGalleryView.svelte b/packages/desktop/views/dashboard/collectibles/views/CollectionsGalleryView.svelte index e95024b661..28f84f689d 100644 --- a/packages/desktop/views/dashboard/collectibles/views/CollectionsGalleryView.svelte +++ b/packages/desktop/views/dashboard/collectibles/views/CollectionsGalleryView.svelte @@ -1,13 +1,13 @@ @@ -35,23 +33,25 @@
{localize('views.collectibles.collectionsGallery.title')} - {String(queriedCollections.length ?? '')} + {String(Object.keys($selectedAccountCollections).length ?? '')}
- {#if collections.length} - - + {#if hasCollections} + {/if} {#if features.collectibles.erc721.enabled} {/if}
- {#if collections.length} - {#if queriedCollections.length} + {#if hasCollections} + {#if Object.keys(queriedCollections).length > 0} + {#each Object.keys(queriedCollections) as collection} + {queriedCollections[collection].name} + {/each} {:else}
{ - let hasEnoughFunds = false + let hasEnoughFunds = true if (selectedToken && isEvmNetwork(selectedToken.networkId)) { hasEnoughFunds = await canAccountMakeEvmTransaction( $selectedAccountIndex, diff --git a/packages/shared/src/lib/core/layer-2/actions/fetchL2BalanceForAccount.ts b/packages/shared/src/lib/core/layer-2/actions/fetchL2BalanceForAccount.ts index a0102ef16b..758584959b 100644 --- a/packages/shared/src/lib/core/layer-2/actions/fetchL2BalanceForAccount.ts +++ b/packages/shared/src/lib/core/layer-2/actions/fetchL2BalanceForAccount.ts @@ -23,7 +23,7 @@ export function fetchL2BalanceForAccount(profileId: string, account: IAccountSta } if (features.collectibles.erc721.enabled) { - void updateErc721NftsOwnership(account) + void updateErc721NftsOwnership(account, evmNetwork.id) } const l2TokenBalance = isIscChain(evmNetwork) diff --git a/packages/shared/src/lib/core/network/classes/isc-chain.class.ts b/packages/shared/src/lib/core/network/classes/isc-chain.class.ts index b2a2c18fee..7774f8fa0e 100644 --- a/packages/shared/src/lib/core/network/classes/isc-chain.class.ts +++ b/packages/shared/src/lib/core/network/classes/isc-chain.class.ts @@ -14,8 +14,7 @@ export class IscChain extends BaseEvmNetwork { try { const { rpcEndpoint, aliasAddress, apiEndpoint } = chainConfiguration const _rpcEndpoint = `${rpcEndpoint}/v1/chains/${aliasAddress}/evm` - chainConfiguration.rpcEndpoint = _rpcEndpoint - super(chainConfiguration) + super({ ...chainConfiguration, rpcEndpoint: _rpcEndpoint }) this.aliasAddress = aliasAddress this.apiEndpoint = apiEndpoint diff --git a/packages/shared/src/lib/core/nfts/actions/updateErc721NftOwnership.ts b/packages/shared/src/lib/core/nfts/actions/updateErc721NftOwnership.ts index cee6632566..b3c65cd83c 100644 --- a/packages/shared/src/lib/core/nfts/actions/updateErc721NftOwnership.ts +++ b/packages/shared/src/lib/core/nfts/actions/updateErc721NftOwnership.ts @@ -5,12 +5,13 @@ import { IErc721Nft } from '../interfaces' import { getAllAccountNfts, persistedNftForActiveProfile, updatePersistedNft } from '../stores' import { getOwnerOfErc721Nft } from '../utils' import { get } from 'svelte/store' +import { NetworkId } from '@core/network' -export async function updateErc721NftsOwnership(account: IAccountState): Promise { +export async function updateErc721NftsOwnership(account: IAccountState, networkId: NetworkId): Promise { try { const trackedErc721Nfts = (getAllAccountNfts()[account.index]?.filter((nft) => { - return nft.standard === NftStandard.Erc721 + return nft.standard === NftStandard.Erc721 && nft.networkId === networkId }) as IErc721Nft[]) ?? [] const promises = trackedErc721Nfts.map(async (nft) => { const updatedOwner = await getOwnerOfErc721Nft(nft) diff --git a/packages/shared/src/lib/core/nfts/interfaces/collection.interface.ts b/packages/shared/src/lib/core/nfts/interfaces/collection.interface.ts new file mode 100644 index 0000000000..0d392293d2 --- /dev/null +++ b/packages/shared/src/lib/core/nfts/interfaces/collection.interface.ts @@ -0,0 +1,10 @@ +import { NftStandard } from '../enums' +import { Nft } from './nft.interface' + +export interface Collection { + standard: NftStandard + name: string + type: string + uri: string + nfts: Nft[] +} diff --git a/packages/shared/src/lib/core/nfts/interfaces/index.ts b/packages/shared/src/lib/core/nfts/interfaces/index.ts index 6d0e272141..0daf27508b 100644 --- a/packages/shared/src/lib/core/nfts/interfaces/index.ts +++ b/packages/shared/src/lib/core/nfts/interfaces/index.ts @@ -1,3 +1,4 @@ +export * from './collection.interface' export * from './download-metadata.interface' export * from './erc721-contract-metadata.interface' export * from './nft-filter.interface' diff --git a/packages/shared/src/lib/core/nfts/interfaces/nft.interface.ts b/packages/shared/src/lib/core/nfts/interfaces/nft.interface.ts index 47183bd6b0..af716bf044 100644 --- a/packages/shared/src/lib/core/nfts/interfaces/nft.interface.ts +++ b/packages/shared/src/lib/core/nfts/interfaces/nft.interface.ts @@ -1,5 +1,4 @@ import { EvmNetworkId, NetworkId } from '@core/network/types' -import { Address } from '@iota/sdk/out/types' import { MimeType, NftStandard } from '../enums' import { IDownloadMetadata, IErc721ContractMetadata, IErc721TokenMetadata, IIrc27Metadata } from '../interfaces' @@ -10,7 +9,7 @@ export interface IIrc27Nft extends IBaseNft { nftAddress: string rawMetadata: string metadata?: IIrc27Metadata - issuer?: Address + issuer?: { type: number; aliasId?: string; nftId?: string } timelockTime?: number expirationTime?: number latestOutputId: string diff --git a/packages/shared/src/lib/core/nfts/stores/index.ts b/packages/shared/src/lib/core/nfts/stores/index.ts index 34585e3cc5..45c2c513d3 100644 --- a/packages/shared/src/lib/core/nfts/stores/index.ts +++ b/packages/shared/src/lib/core/nfts/stores/index.ts @@ -2,6 +2,7 @@ export * from './all-account-nfts.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-nfts.store' export * from './selected-account-nfts.store' export * from './selected-collectibles-tabs.store' diff --git a/packages/shared/src/lib/core/nfts/stores/selected-account-collections.store.ts b/packages/shared/src/lib/core/nfts/stores/selected-account-collections.store.ts new file mode 100644 index 0000000000..d537ca49d7 --- /dev/null +++ b/packages/shared/src/lib/core/nfts/stores/selected-account-collections.store.ts @@ -0,0 +1,58 @@ +import { derived, get, Readable, Writable, writable } from 'svelte/store' +import { selectedAccountNfts } from './selected-account-nfts.store' +import { NftStandard } from '../enums' +import { Nft } from '../interfaces' +import { getCollectionFromNft } from '../utils' +import { Collections } from '../types' + +export const collectionsStore: Writable = writable({}) + +async function updateCollections(nfts: Nft[]): Promise { + const existingCollections = get(collectionsStore) + + if (nfts.length === 0) { + if (Object.keys(existingCollections).length > 0) { + collectionsStore.set({}) + } + return + } + + const collectionsUpdate = { ...existingCollections } + + await Promise.all( + nfts.map(async (nft) => { + if (nft.standard !== NftStandard.Irc27 || !nft.issuer) { + return + } + + const issuerId = nft.issuer.aliasId ?? nft.issuer.nftId + if (!issuerId) { + return + } + + if (!collectionsUpdate[issuerId]) { + const collection = await getCollectionFromNft(nft) + if (collection) { + collectionsUpdate[issuerId] = { ...collection, nfts: [nft] } + } + } else { + const existingNfts = collectionsUpdate[issuerId].nfts + if (!existingNfts.find((existingNft) => existingNft.id === nft.id)) { + collectionsUpdate[issuerId].nfts.push(nft) + } + } + }) + ) + collectionsStore.set(collectionsUpdate) +} + +selectedAccountNfts.subscribe((nfts) => { + void updateCollections(nfts) +}) + +export const selectedAccountCollections: Readable = derived( + collectionsStore, + ($collectionsStore) => $collectionsStore +) + +export const collectionsSearchTerm: Writable = writable('') diff --git a/packages/shared/src/lib/core/nfts/types/collections.type.ts b/packages/shared/src/lib/core/nfts/types/collections.type.ts new file mode 100644 index 0000000000..32dae6f7df --- /dev/null +++ b/packages/shared/src/lib/core/nfts/types/collections.type.ts @@ -0,0 +1,3 @@ +import { Collection } from '../interfaces' + +export type Collections = { [key: string]: Collection } diff --git a/packages/shared/src/lib/core/nfts/types/index.ts b/packages/shared/src/lib/core/nfts/types/index.ts index 3d34a70382..77e3cbecb6 100644 --- a/packages/shared/src/lib/core/nfts/types/index.ts +++ b/packages/shared/src/lib/core/nfts/types/index.ts @@ -1,2 +1,3 @@ +export * from './collections.type' export * from './nft-download-options.type' export * from './persisted-nft.type' diff --git a/packages/shared/src/lib/core/nfts/utils/getCollectionFromNft.ts b/packages/shared/src/lib/core/nfts/utils/getCollectionFromNft.ts new file mode 100644 index 0000000000..232f4d632e --- /dev/null +++ b/packages/shared/src/lib/core/nfts/utils/getCollectionFromNft.ts @@ -0,0 +1,42 @@ +import { NftStandard } from '../enums' +import { Collection, Nft } from '../interfaces' +import { Converter } from '@core/utils' +import { getClient } from '@core/profile-manager' +import type { AliasOutput, MetadataFeature, NftOutput } from '@iota/sdk' +import { FeatureType } from '@iota/sdk/out/types' + +export async function getCollectionFromNft(nft: Nft): Promise { + if (nft.standard !== NftStandard.Irc27) { + return + } + + const { aliasId = '', nftId = '' } = nft.issuer ?? {} + if (!aliasId && !nftId) { + return + } + + try { + const client = await getClient() + const outputId = aliasId ? await client.aliasOutputId(aliasId) : await client.nftOutputId(nftId) + if (!outputId) { + return + } + + const outputResponse = await client.getOutput(outputId) + const output = outputResponse.output as AliasOutput | NftOutput + + const metadataFeature = output.immutableFeatures?.find( + (feature) => feature.type === FeatureType.Metadata + ) as MetadataFeature + + if (!metadataFeature?.data) { + return + } + + const { standard, name, type, uri } = JSON.parse(Converter.hexToUtf8(metadataFeature.data)) + + return { standard, name, type, uri, nfts: [] } + } catch (error) { + console.error('Error retrieving collection from NFT:', error) + } +} diff --git a/packages/shared/src/lib/core/nfts/utils/index.ts b/packages/shared/src/lib/core/nfts/utils/index.ts index 93e479f6d5..f8305f6b34 100644 --- a/packages/shared/src/lib/core/nfts/utils/index.ts +++ b/packages/shared/src/lib/core/nfts/utils/index.ts @@ -1,17 +1,19 @@ export * from './buildNftFromPersistedErc721Nft' +export * from './buildPersistedErc721Nft' export * from './checkIfNftShouldBeDownloaded' +export * from './fetchWithTimeout' +export * from './getCollectionFromNft' +export * from './getFetchableNftUrls' export * from './getFilePathForNft' export * from './getNftsFromNftIds' export * from './getOwnerOfErc721Nft' -export * from './buildPersistedErc721Nft' -export * from './getFetchableNftUrls' export * from './getPrimaryNftUrl' export * from './getSpendableStatusFromUnspentNftOutput' -export * from './fetchWithTimeout' export * from './isIrc27Nft' -export * from './isNftOwnedByAnyAccount' export * from './isNftLocked' +export * from './isNftOwnedByAnyAccount' export * from './isScamIrc27Nft' export * from './isValidNftUri' +export * from './isVisibleCollection' export * from './isVisibleNft' export * from './parseNftMetadata' diff --git a/packages/shared/src/lib/core/nfts/utils/isVisibleCollection.ts b/packages/shared/src/lib/core/nfts/utils/isVisibleCollection.ts new file mode 100644 index 0000000000..6c367e4cbd --- /dev/null +++ b/packages/shared/src/lib/core/nfts/utils/isVisibleCollection.ts @@ -0,0 +1,20 @@ +import { get } from 'svelte/store' +import { collectionsSearchTerm } from '../stores' +import { Collection } from '../interfaces' + +export function isVisibleCollection(collection: Collection): boolean { + const searchTerm = get(collectionsSearchTerm) + + if (!isVisibleWithSearchTerm(collection, searchTerm)) { + return false + } + + return true +} + +function isVisibleWithSearchTerm(collection: Collection, searchTerm: string): boolean { + if (searchTerm) { + return collection.name.toLowerCase().includes(searchTerm.toLowerCase()) + } + return true +}