Skip to content

Commit

Permalink
feat: add error handling for failed token metadata fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
jnsdls committed Feb 26, 2024
1 parent 525a2e2 commit 9cc86f7
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 12 deletions.
6 changes: 5 additions & 1 deletion packages/thirdweb/src/extensions/erc1155/read/getNFT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion packages/thirdweb/src/extensions/erc721/read/getNFT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
229 changes: 228 additions & 1 deletion packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,241 @@
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");

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,
Expand Down
15 changes: 7 additions & 8 deletions packages/thirdweb/src/utils/nft/fetchTokenMetadata.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,7 +16,9 @@ type FetchTokenMetadataOptions = {
* @returns The token metadata.
* @internal
*/
export async function fetchTokenMetadata(options: FetchTokenMetadataOptions) {
export async function fetchTokenMetadata(
options: FetchTokenMetadataOptions,
): Promise<NFTMetadata> {
const { client, tokenId, tokenUri } = options;
// handle case where the URI is a base64 encoded JSON (onchain nft)
if (isBase64JSON(tokenUri)) {
Expand All @@ -31,7 +30,7 @@ export async function fetchTokenMetadata(options: FetchTokenMetadataOptions) {
{ tokenId, tokenUri },
e,
);
return FALLBACK_METADATA;
throw e;
}
}

Expand All @@ -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)
Expand All @@ -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;
}
}
2 changes: 1 addition & 1 deletion packages/thirdweb/src/utils/nft/parseNft.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type NFTMetadata = {
export type NFTMetadata = {
id: bigint;
uri: string;
name?: string;
Expand Down
8 changes: 8 additions & 0 deletions packages/thirdweb/test/src/test-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down

0 comments on commit 9cc86f7

Please sign in to comment.