Skip to content

Commit

Permalink
refactor: improve error handling for ERC721 NFT tracking (#1567)
Browse files Browse the repository at this point in the history
* Add better error handling for NFT tracking

* chore: remove default token ID fallback

* fix: types

---------

Co-authored-by: Tuditi <[email protected]>
Co-authored-by: Tuditi <[email protected]>
  • Loading branch information
3 people authored Dec 6, 2023
1 parent ba6a70e commit c638ece
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@ import { IAccountState } from '@core/account/interfaces'
import { checkForUntrackedNfts } from '@core/nfts/actions'
import { LAYER2_TOKENS_POLL_INTERVAL } from '../constants'
import { checkForUntrackedTokens, fetchL2BalanceForAccount } from '.'
import { handleError } from '@core/error/handlers'
import { IError } from '@core/error'

let pollInterval: number

export function pollL2BalanceForAccount(account: IAccountState): void {
clearL2TokensPoll()
checkForUntrackedTokens(account)
checkForUntrackedNfts(account)
fetchL2BalanceForAccount(account)
pollInterval = window.setInterval(() => {
try {
clearL2TokensPoll()
checkForUntrackedTokens(account)
checkForUntrackedNfts(account)
fetchL2BalanceForAccount(account)
}, LAYER2_TOKENS_POLL_INTERVAL)
pollInterval = window.setInterval(() => {
fetchL2BalanceForAccount(account)
}, LAYER2_TOKENS_POLL_INTERVAL)
} catch (err) {
handleError(err as IError)
}
}

export function clearL2TokensPoll(): void {
Expand Down
71 changes: 43 additions & 28 deletions packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { IAccountState } from '@core/account/interfaces'
import { ContractType, Erc721InterfaceId } from '@core/layer-2/enums'
import { ContractType } from '@core/layer-2/enums'
import { EvmExplorerApi } from '@core/network/classes'
import { getNetwork } from '@core/network/stores'
import { TokenTrackingStatus } from '@core/token/enums'

import { DEFAULT_NFT_TOKEN_ID } from '../constants'
import { NftStandard } from '../enums'
import { IErc721ContractMetadata } from '../interfaces'
import { addPersistedNft } from '../stores'
import { getPersistedErc721NftFromContract } from '../utils'
import { addNewTrackedNftToActiveProfile } from './addNewTrackedNftToActiveProfile'
import { isNftPersisted } from './isNftPersisted'
import { Contract } from '@core/layer-2'
import { IExplorerAssetMetadata } from '@core/network'
import { IChain, IExplorerAsset } from '@core/network'

export function checkForUntrackedNfts(account: IAccountState): void {
const chains = getNetwork()?.getChains() ?? []
Expand All @@ -27,38 +26,54 @@ export function checkForUntrackedNfts(account: IAccountState): void {

const explorerNfts = await explorerApi.getAssetsForAddress(evmAddress, NftStandard.Erc721)
for (const explorerNft of explorerNfts) {
const { token, value } = explorerNft
const { address } = token
const contract = chain.getContract(ContractType.Erc721, address)

const isEnumerable = await contract.methods.supportsInterface(Erc721InterfaceId.Enumerable).call()
if (isEnumerable) {
await Promise.all(
Array.from({ length: Number(value) }).map(async (_, idx) => {
const tokenId = await contract.methods.tokenOfOwnerByIndex(evmAddress, idx).call()
await persistNft(token, tokenId, contract)
})
)
} else {
await persistNft(token, DEFAULT_NFT_TOKEN_ID, contract)
}

addNewTrackedNftToActiveProfile(networkId, address, TokenTrackingStatus.AutomaticallyTracked)
void persistNftsFromExplorerAsset(evmAddress, explorerNft, chain)
}
})
}

async function persistNft(token: IExplorerAssetMetadata, tokenId: string, contract: Contract): Promise<void> {
async function persistNftsFromExplorerAsset(evmAddress: string, asset: IExplorerAsset, chain: IChain): Promise<void> {
const { token, value } = asset
const { address, name, symbol } = token
if (isNftPersisted(address, tokenId)) {
return
try {
const contract = chain.getContract(ContractType.Erc721, address)

await Promise.all(
Array.from({ length: Number(value) }).map(async (_, idx) => {
try {
const tokenId = await contract.methods.tokenOfOwnerByIndex(evmAddress, idx).call()
await persistNftWithContractMetadata(
{
standard: NftStandard.Erc721,
address,
name,
symbol,
},
tokenId,
contract
)
} catch (err) {
// If we don't have the tokenId we cannot persist the NFT. ERC-721 contracts should implement
// the ERC-165 interface to support `tokenOfOwnerByIndex`
// https://stackoverflow.com/questions/69302924/erc-721-how-to-get-all-token-ids
}
})
)

addNewTrackedNftToActiveProfile(chain.getConfiguration().id, address, TokenTrackingStatus.AutomaticallyTracked)
} catch (err) {
console.error(err)
throw new Error(`Unable to persist NFT with address ${address}`)
}
}

const contractMetadata: IErc721ContractMetadata = {
standard: NftStandard.Erc721,
address,
name,
symbol,
async function persistNftWithContractMetadata(
contractMetadata: IErc721ContractMetadata,
tokenId: string,
contract: Contract
): Promise<void> {
const { address } = contractMetadata
if (!tokenId || isNftPersisted(address, tokenId)) {
return
}
addPersistedNft(
`${address}:${tokenId}`,
Expand Down
5 changes: 2 additions & 3 deletions packages/shared/src/lib/core/nfts/actions/isNftPersisted.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { get } from 'svelte/store'
import { DEFAULT_NFT_TOKEN_ID } from '../constants'
import { persistedNftForActiveProfile } from '../stores'

export function isNftPersisted(nftId: string, tokenId?: string): boolean {
return `${nftId}:${tokenId ?? DEFAULT_NFT_TOKEN_ID}` in get(persistedNftForActiveProfile)
export function isNftPersisted(nftId: string, tokenId: string): boolean {
return `${nftId}:${tokenId}` in (get(persistedNftForActiveProfile) ?? {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,26 @@ export async function getPersistedErc721NftFromContract(

const hasTokenMetadata = await contract.methods.supportsInterface(Erc721InterfaceId.Metadata).call()
if (hasTokenMetadata) {
const tokenUri = await contract.methods.tokenURI(tokenId).call()
const composedTokenUri = composeUrlFromNftUri(tokenUri)
persistedNft.tokenUri = composedTokenUri
try {
const tokenUri = await contract.methods.tokenURI(tokenId).call()
const composedTokenUri = composeUrlFromNftUri(tokenUri)
persistedNft.tokenUri = composedTokenUri

const response = await fetch(composedTokenUri)
const metadata = (await response.json()) as IErc721TokenMetadata
if (metadata) {
const attributes: IErc721TokenMetadataAttribute[] = metadata.attributes?.map((attribute) => ({
traitType: attribute['trait_type'],
value: attribute.value,
}))
persistedNft.tokenMetadata = {
...metadata,
image: composeUrlFromNftUri(metadata.image) ?? metadata.image,
attributes,
const response = await fetch(composedTokenUri)
const metadata = (await response.json()) as IErc721TokenMetadata
if (metadata) {
const attributes: IErc721TokenMetadataAttribute[] = metadata.attributes?.map((attribute) => ({
traitType: attribute['trait_type'],
value: attribute.value,
}))
persistedNft.tokenMetadata = {
...metadata,
image: composeUrlFromNftUri(metadata.image) ?? metadata.image,
attributes,
}
}
} catch (err) {
throw new Error(`Unable to get metadata of token ${tokenId} from contract ${contractMetadata.address}`)
}
}

Expand Down

0 comments on commit c638ece

Please sign in to comment.