From 9cc86f7078084920484fb7f99eca0de148f77818 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Mon, 26 Feb 2024 14:34:42 -0800 Subject: [PATCH] feat: add error handling for failed token metadata fetch --- .../src/extensions/erc1155/read/getNFT.ts | 6 +- .../src/extensions/erc721/read/getNFT.ts | 6 +- .../extensions/erc721/read/getNFTs.test.ts | 229 +++++++++++++++++- .../src/utils/nft/fetchTokenMetadata.ts | 15 +- packages/thirdweb/src/utils/nft/parseNft.ts | 2 +- packages/thirdweb/test/src/test-contracts.ts | 8 + 6 files changed, 254 insertions(+), 12 deletions(-) diff --git a/packages/thirdweb/src/extensions/erc1155/read/getNFT.ts b/packages/thirdweb/src/extensions/erc1155/read/getNFT.ts index c677454b25e..3b0a7a0d2f2 100644 --- a/packages/thirdweb/src/extensions/erc1155/read/getNFT.ts +++ b/packages/thirdweb/src/extensions/erc1155/read/getNFT.ts @@ -35,7 +35,11 @@ export async function getNFT( client: options.contract.client, tokenId: options.tokenId, tokenUri, - }), + }).catch(() => ({ + id: options.tokenId, + type: "ERC1155", + uri: tokenUri, + })), { tokenId: options.tokenId, tokenUri, diff --git a/packages/thirdweb/src/extensions/erc721/read/getNFT.ts b/packages/thirdweb/src/extensions/erc721/read/getNFT.ts index f9569e0f095..d48a24efd9e 100644 --- a/packages/thirdweb/src/extensions/erc721/read/getNFT.ts +++ b/packages/thirdweb/src/extensions/erc721/read/getNFT.ts @@ -61,7 +61,11 @@ export async function getNFT( client: options.contract.client, tokenId: options.tokenId, tokenUri: uri, - }).catch(() => {}), + }).catch(() => ({ + id: options.tokenId, + type: "ERC721", + uri, + })), { tokenId: options.tokenId, tokenUri: uri, diff --git a/packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts b/packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts index ec932a3bb4c..3c39d10a681 100644 --- a/packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts +++ b/packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, afterEach } from "vitest"; import { getNFTs } from "./getNFTs.js"; -import { DOODLES_CONTRACT } from "~test/test-contracts.js"; +import { AZUKI_CONTRACT, DOODLES_CONTRACT } from "~test/test-contracts.js"; const fetchSpy = vi.spyOn(globalThis, "fetch"); @@ -9,6 +9,233 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => { afterEach(() => { fetchSpy.mockClear(); }); + + it("works for azuki", async () => { + const nfts = await getNFTs({ + contract: AZUKI_CONTRACT, + count: 5, + }); + + expect(nfts.length).toBe(5); + expect(nfts).toMatchInlineSnapshot(` + [ + { + "id": 0n, + "metadata": { + "attributes": [ + { + "trait_type": "Type", + "value": "Human", + }, + { + "trait_type": "Hair", + "value": "Water", + }, + { + "trait_type": "Clothing", + "value": "Pink Oversized Kimono", + }, + { + "trait_type": "Eyes", + "value": "Striking", + }, + { + "trait_type": "Mouth", + "value": "Frown", + }, + { + "trait_type": "Offhand", + "value": "Monkey King Staff", + }, + { + "trait_type": "Background", + "value": "Off White A", + }, + ], + "image": "ipfs://QmYDvPAXtiJg7s8JdRBSLWdgSphQdac8j1YuQNNxcGE1hg/0.png", + "name": "Azuki #0", + }, + "owner": null, + "supply": 1n, + "tokenURI": "ipfs://QmZcH4YvBVVRJtdn4RdbaqgspFU8gH6P9vomDpBVpAL3u4/0", + "type": "ERC721", + }, + { + "id": 1n, + "metadata": { + "attributes": [ + { + "trait_type": "Type", + "value": "Human", + }, + { + "trait_type": "Hair", + "value": "Pink Hairband", + }, + { + "trait_type": "Clothing", + "value": "White Qipao with Fur", + }, + { + "trait_type": "Eyes", + "value": "Daydreaming", + }, + { + "trait_type": "Mouth", + "value": "Lipstick", + }, + { + "trait_type": "Offhand", + "value": "Gloves", + }, + { + "trait_type": "Background", + "value": "Off White D", + }, + ], + "image": "ipfs://QmYDvPAXtiJg7s8JdRBSLWdgSphQdac8j1YuQNNxcGE1hg/1.png", + "name": "Azuki #1", + }, + "owner": null, + "supply": 1n, + "tokenURI": "ipfs://QmZcH4YvBVVRJtdn4RdbaqgspFU8gH6P9vomDpBVpAL3u4/1", + "type": "ERC721", + }, + { + "id": 2n, + "metadata": { + "attributes": [ + { + "trait_type": "Type", + "value": "Human", + }, + { + "trait_type": "Hair", + "value": "Pink Flowy", + }, + { + "trait_type": "Ear", + "value": "Red Tassel", + }, + { + "trait_type": "Clothing", + "value": "Vest", + }, + { + "trait_type": "Eyes", + "value": "Ruby", + }, + { + "trait_type": "Mouth", + "value": "Chewing", + }, + { + "trait_type": "Background", + "value": "Red", + }, + ], + "image": "ipfs://QmYDvPAXtiJg7s8JdRBSLWdgSphQdac8j1YuQNNxcGE1hg/2.png", + "name": "Azuki #2", + }, + "owner": null, + "supply": 1n, + "tokenURI": "ipfs://QmZcH4YvBVVRJtdn4RdbaqgspFU8gH6P9vomDpBVpAL3u4/2", + "type": "ERC721", + }, + { + "id": 3n, + "metadata": { + "attributes": [ + { + "trait_type": "Type", + "value": "Human", + }, + { + "trait_type": "Hair", + "value": "Green Spiky", + }, + { + "trait_type": "Headgear", + "value": "Frog Headband", + }, + { + "trait_type": "Neck", + "value": "Frog Headphones", + }, + { + "trait_type": "Clothing", + "value": "Green Yukata", + }, + { + "trait_type": "Eyes", + "value": "Careless", + }, + { + "trait_type": "Mouth", + "value": "Grass", + }, + { + "trait_type": "Offhand", + "value": "Katana", + }, + { + "trait_type": "Background", + "value": "Red", + }, + ], + "image": "ipfs://QmYDvPAXtiJg7s8JdRBSLWdgSphQdac8j1YuQNNxcGE1hg/3.png", + "name": "Azuki #3", + }, + "owner": null, + "supply": 1n, + "tokenURI": "ipfs://QmZcH4YvBVVRJtdn4RdbaqgspFU8gH6P9vomDpBVpAL3u4/3", + "type": "ERC721", + }, + { + "id": 4n, + "metadata": { + "attributes": [ + { + "trait_type": "Type", + "value": "Human", + }, + { + "trait_type": "Hair", + "value": "Brown Dreadlocks", + }, + { + "trait_type": "Clothing", + "value": "White Qipao with Fur", + }, + { + "trait_type": "Eyes", + "value": "Lightning", + }, + { + "trait_type": "Mouth", + "value": "Smirk", + }, + { + "trait_type": "Offhand", + "value": "Katana", + }, + { + "trait_type": "Background", + "value": "Off White D", + }, + ], + "image": "ipfs://QmYDvPAXtiJg7s8JdRBSLWdgSphQdac8j1YuQNNxcGE1hg/4.png", + "name": "Azuki #4", + }, + "owner": null, + "supply": 1n, + "tokenURI": "ipfs://QmZcH4YvBVVRJtdn4RdbaqgspFU8gH6P9vomDpBVpAL3u4/4", + "type": "ERC721", + }, + ] + `); + }); + it("works for a contract with 0 indexed NFTs", async () => { const nfts = await getNFTs({ contract: DOODLES_CONTRACT, diff --git a/packages/thirdweb/src/utils/nft/fetchTokenMetadata.ts b/packages/thirdweb/src/utils/nft/fetchTokenMetadata.ts index c1f8771a7c1..245fb161393 100644 --- a/packages/thirdweb/src/utils/nft/fetchTokenMetadata.ts +++ b/packages/thirdweb/src/utils/nft/fetchTokenMetadata.ts @@ -1,10 +1,7 @@ import { isBase64JSON, parseBase64String } from "../base64/base64.js"; import type { ThirdwebClient } from "../../client/client.js"; import { numberToHex } from "../encoding/hex.js"; - -const FALLBACK_METADATA = { - name: "Failed to load NFT metadata", -}; +import type { NFTMetadata } from "./parseNft.js"; type FetchTokenMetadataOptions = { client: ThirdwebClient; @@ -19,7 +16,9 @@ type FetchTokenMetadataOptions = { * @returns The token metadata. * @internal */ -export async function fetchTokenMetadata(options: FetchTokenMetadataOptions) { +export async function fetchTokenMetadata( + options: FetchTokenMetadataOptions, +): Promise { const { client, tokenId, tokenUri } = options; // handle case where the URI is a base64 encoded JSON (onchain nft) if (isBase64JSON(tokenUri)) { @@ -31,7 +30,7 @@ export async function fetchTokenMetadata(options: FetchTokenMetadataOptions) { { tokenId, tokenUri }, e, ); - return FALLBACK_METADATA; + throw e; } } @@ -45,7 +44,7 @@ export async function fetchTokenMetadata(options: FetchTokenMetadataOptions) { } } catch (e) { console.error("Failed to fetch non-dynamic NFT", { tokenId, tokenUri }, e); - return FALLBACK_METADATA; + throw e; } // DYNAMIC NFT FORMATS (2 options, sadly has to be waterfall) @@ -72,6 +71,6 @@ export async function fetchTokenMetadata(options: FetchTokenMetadataOptions) { } } catch (e) { console.error("Failed to fetch dynamic NFT", { tokenId, tokenUri }, e); - return FALLBACK_METADATA; + throw e; } } diff --git a/packages/thirdweb/src/utils/nft/parseNft.ts b/packages/thirdweb/src/utils/nft/parseNft.ts index 3c1daf953bb..501140bacac 100644 --- a/packages/thirdweb/src/utils/nft/parseNft.ts +++ b/packages/thirdweb/src/utils/nft/parseNft.ts @@ -1,4 +1,4 @@ -type NFTMetadata = { +export type NFTMetadata = { id: bigint; uri: string; name?: string; diff --git a/packages/thirdweb/test/src/test-contracts.ts b/packages/thirdweb/test/src/test-contracts.ts index f58833cdfd8..c424b8d31f7 100644 --- a/packages/thirdweb/test/src/test-contracts.ts +++ b/packages/thirdweb/test/src/test-contracts.ts @@ -30,6 +30,14 @@ export const DOODLES_CONTRACT = getContract({ chain: FORKED_ETHEREUM_CHAIN, }); +const AZUKI_ADDRESS = "0xED5AF388653567Af2F388E6224dC7C4b3241C544"; + +export const AZUKI_CONTRACT = getContract({ + client: TEST_CLIENT, + address: AZUKI_ADDRESS, + chain: FORKED_ETHEREUM_CHAIN, +}); + // ERC1155 const AURA_ADDRESS = "0x42d3641255C946CC451474295d29D3505173F22A";