diff --git a/.changeset/huge-berries-call.md b/.changeset/huge-berries-call.md new file mode 100644 index 00000000000..f5ef003c50d --- /dev/null +++ b/.changeset/huge-berries-call.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Use insight for 1155 getNFTs, getOwnedNFTs and getNFT diff --git a/packages/thirdweb/src/extensions/erc1155/read/getNFT.test.ts b/packages/thirdweb/src/extensions/erc1155/read/getNFT.test.ts index 55039f1ec85..4eae2cfba03 100644 --- a/packages/thirdweb/src/extensions/erc1155/read/getNFT.test.ts +++ b/packages/thirdweb/src/extensions/erc1155/read/getNFT.test.ts @@ -4,10 +4,21 @@ import { DROP1155_CONTRACT } from "~test/test-contracts.js"; import { getNFT } from "./getNFT.js"; describe.runIf(process.env.TW_SECRET_KEY)("erc1155.getNFT", () => { + it("without owner with indexer", async () => { + const nft = await getNFT({ + contract: DROP1155_CONTRACT, + tokenId: 2n, + }); + expect(nft.metadata.name).toBe("Aura Platinum"); + // biome-ignore lint/suspicious/noExplicitAny: todo type this better + expect((nft as any).supply).toBe(2519n); + }); + it("without owner", async () => { const nft = await getNFT({ contract: DROP1155_CONTRACT, tokenId: 2n, + useIndexer: false, }); expect(nft).toMatchInlineSnapshot(` { diff --git a/packages/thirdweb/src/extensions/erc1155/read/getNFT.ts b/packages/thirdweb/src/extensions/erc1155/read/getNFT.ts index 63f1d48d105..e51f8b5e57c 100644 --- a/packages/thirdweb/src/extensions/erc1155/read/getNFT.ts +++ b/packages/thirdweb/src/extensions/erc1155/read/getNFT.ts @@ -1,9 +1,9 @@ +import { getNFT as getNFTInsight } from "../../../insight/get-nfts.js"; import type { BaseTransactionOptions } from "../../../transaction/types.js"; import { fetchTokenMetadata } from "../../../utils/nft/fetchTokenMetadata.js"; import { type NFT, parseNFT } from "../../../utils/nft/parseNft.js"; import { totalSupply } from "../__generated__/IERC1155/read/totalSupply.js"; import { uri } from "../__generated__/IERC1155/read/uri.js"; - export { isUriSupported as isGetNFTSupported } from "../__generated__/IERC1155/read/uri.js"; /** @@ -12,6 +12,11 @@ export { isUriSupported as isGetNFTSupported } from "../__generated__/IERC1155/r */ export type GetNFTParams = { tokenId: bigint; + /** + * Whether to use the insight API to fetch the NFT. + * @default true + */ + useIndexer?: boolean; }; /** @@ -30,6 +35,51 @@ export type GetNFTParams = { */ export async function getNFT( options: BaseTransactionOptions, +): Promise { + const { useIndexer = true } = options; + if (useIndexer) { + try { + return await getNFTFromInsight(options); + } catch { + return await getNFTFromRPC(options); + } + } + return await getNFTFromRPC(options); +} + +async function getNFTFromInsight( + options: BaseTransactionOptions, +): Promise { + const tokenId = options.tokenId; + const nft = await getNFTInsight({ + client: options.contract.client, + chain: options.contract.chain, + contractAddress: options.contract.address, + tokenId: options.tokenId, + }); + if (!nft) { + return parseNFT( + { + id: tokenId, + type: "ERC1155", + uri: "", + }, + { + tokenId: options.tokenId, + tokenUri: "", + type: "ERC1155", + owner: null, + supply: 0n, + tokenAddress: options.contract.address, + chainId: options.contract.chain.id, + }, + ); + } + return nft; +} + +async function getNFTFromRPC( + options: BaseTransactionOptions, ): Promise { const [tokenUri, supply] = await Promise.all([ uri({ diff --git a/packages/thirdweb/src/extensions/erc1155/read/getNFTs.ts b/packages/thirdweb/src/extensions/erc1155/read/getNFTs.ts index 583e75bdd0d..5578c8c5b8c 100644 --- a/packages/thirdweb/src/extensions/erc1155/read/getNFTs.ts +++ b/packages/thirdweb/src/extensions/erc1155/read/getNFTs.ts @@ -1,4 +1,5 @@ import { maxUint256 } from "ox/Solidity"; +import { getContractNFTs } from "../../../insight/get-nfts.js"; import type { BaseTransactionOptions } from "../../../transaction/types.js"; import { min } from "../../../utils/bigint.js"; import type { NFT } from "../../../utils/nft/parseNft.js"; @@ -23,6 +24,11 @@ export type GetNFTsParams = { * The number of NFTs to retrieve. */ count?: number; + /** + * Whether to use the insight API to fetch the NFTs. + * @default true + */ + useIndexer?: boolean; }; /** @@ -42,6 +48,38 @@ export type GetNFTsParams = { */ export async function getNFTs( options: BaseTransactionOptions, +): Promise { + const { useIndexer = true } = options; + if (useIndexer) { + try { + return await getNFTsFromInsight(options); + } catch { + return await getNFTsFromRPC(options); + } + } + return await getNFTsFromRPC(options); +} + +async function getNFTsFromInsight( + options: BaseTransactionOptions, +): Promise { + const { contract, start, count = Number(DEFAULT_QUERY_ALL_COUNT) } = options; + + const result = await getContractNFTs({ + client: contract.client, + chains: [contract.chain], + contractAddress: contract.address, + queryOptions: { + limit: count, + page: start ? Math.floor(start / count) : undefined, + }, + }); + + return result; +} + +async function getNFTsFromRPC( + options: BaseTransactionOptions, ): Promise { const start = BigInt(options.start || 0); const count = BigInt(options.count || DEFAULT_QUERY_ALL_COUNT); diff --git a/packages/thirdweb/src/extensions/erc1155/read/getOwnedNFTs.ts b/packages/thirdweb/src/extensions/erc1155/read/getOwnedNFTs.ts index 80cced26333..3d37007413f 100644 --- a/packages/thirdweb/src/extensions/erc1155/read/getOwnedNFTs.ts +++ b/packages/thirdweb/src/extensions/erc1155/read/getOwnedNFTs.ts @@ -1,3 +1,4 @@ +import { getOwnedNFTs as getInsightNFTs } from "../../../insight/get-nfts.js"; import type { BaseTransactionOptions } from "../../../transaction/types.js"; import type { NFT } from "../../../utils/nft/parseNft.js"; import { getNFT } from "./getNFT.js"; @@ -5,12 +6,17 @@ import { type GetOwnedTokenIdsParams, getOwnedTokenIds, } from "./getOwnedTokenIds.js"; - /** * Parameters for retrieving NFTs. * @extension ERC1155 */ -export type GetOwnedNFTsParams = GetOwnedTokenIdsParams; +export type GetOwnedNFTsParams = GetOwnedTokenIdsParams & { + /** + * Whether to use the insight API to fetch the NFTs. + * @default true + */ + useIndexer?: boolean; +}; /** * Retrieves the owned ERC1155 NFTs for a given wallet address. @@ -30,6 +36,63 @@ export type GetOwnedNFTsParams = GetOwnedTokenIdsParams; */ export async function getOwnedNFTs( options: BaseTransactionOptions, +): Promise<(NFT & { quantityOwned: bigint })[]> { + const { useIndexer = true } = options; + if (useIndexer) { + try { + return await getOwnedNFTsFromInsight(options); + } catch { + return await getOwnedNFTsFromRPC(options); + } + } + return await getOwnedNFTsFromRPC(options); +} + +async function getOwnedNFTsFromInsight( + options: BaseTransactionOptions, +): Promise<(NFT & { quantityOwned: bigint })[]> { + const limit = 50; + const nfts: (NFT & { quantityOwned: bigint })[] = []; + let page = 0; + let hasMore = true; + + // TODO (insight): add support for contract address filters + while (hasMore) { + const pageResults = await getInsightNFTs({ + client: options.contract.client, + chains: [options.contract.chain], + ownerAddress: options.address, + queryOptions: { + limit, + page, + }, + }); + + nfts.push(...pageResults); + + // If we got fewer results than the limit, we've reached the end + if (pageResults.length < limit) { + hasMore = false; + } else { + page++; + } + } + + const results = nfts; + + return results + .filter( + (n) => + n.tokenAddress.toLowerCase() === options.contract.address.toLowerCase(), + ) + .map((result) => ({ + ...result, + owner: options.address, + })); +} + +async function getOwnedNFTsFromRPC( + options: BaseTransactionOptions, ): Promise<(NFT & { quantityOwned: bigint })[]> { const ownedBalances = await getOwnedTokenIds(options); diff --git a/packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts b/packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts index 621a0203e40..c5af41221a0 100644 --- a/packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts +++ b/packages/thirdweb/src/extensions/erc721/read/getNFT.test.ts @@ -10,46 +10,45 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => { includeOwner: false, }); expect(nft.metadata.name).toBe("Doodle #1"); - // TODO (insight): re-enable once insight fixes the client id caching issue - // expect(nft).toMatchInlineSnapshot(` - // { - // "chainId": 1, - // "id": 1n, - // "metadata": { - // "attributes": [ - // { - // "trait_type": "face", - // "value": "holographic beard", - // }, - // { - // "trait_type": "hair", - // "value": "white bucket cap", - // }, - // { - // "trait_type": "body", - // "value": "purple sweater with satchel", - // }, - // { - // "trait_type": "background", - // "value": "grey", - // }, - // { - // "trait_type": "head", - // "value": "gradient 2", - // }, - // ], - // "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", - // "image": "https://${clientId}.ipfscdn.io/ipfs/QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", - // "image_url": "https://${clientId}.ipfscdn.io/ipfs/QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", - // "name": "Doodle #1", - // "uri": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", - // }, - // "owner": null, - // "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", - // "tokenURI": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", - // "type": "ERC721", - // } - // `); + expect(nft).toMatchInlineSnapshot(` + { + "chainId": 1, + "id": 1n, + "metadata": { + "attributes": [ + { + "trait_type": "face", + "value": "holographic beard", + }, + { + "trait_type": "hair", + "value": "white bucket cap", + }, + { + "trait_type": "body", + "value": "purple sweater with satchel", + }, + { + "trait_type": "background", + "value": "grey", + }, + { + "trait_type": "head", + "value": "gradient 2", + }, + ], + "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", + "image": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", + "image_url": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", + "name": "Doodle #1", + "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", + }, + "owner": null, + "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", + "tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", + "type": "ERC721", + } + `); }); it("with owner using indexer", async () => { @@ -60,49 +59,48 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => { }); expect(nft.metadata.name).toBe("Doodle #1"); expect(nft.owner).toBe("0xbe9936fcfc50666f5425fde4a9decc59cef73b24"); - // TODO (insight): re-enable once insight fixes the client id caching issue - // expect(nft).toMatchInlineSnapshot(` - // { - // "chainId": 1, - // "id": 1n, - // "metadata": { - // "attributes": [ - // { - // "trait_type": "face", - // "value": "holographic beard", - // }, - // { - // "trait_type": "hair", - // "value": "white bucket cap", - // }, - // { - // "trait_type": "body", - // "value": "purple sweater with satchel", - // }, - // { - // "trait_type": "background", - // "value": "grey", - // }, - // { - // "trait_type": "head", - // "value": "gradient 2", - // }, - // ], - // "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", - // "image": "https://${clientId}.ipfscdn.io/ipfs/QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", - // "image_url": "https://${clientId}.ipfscdn.io/ipfs/QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", - // "name": "Doodle #1", - // "owner_addresses": [ - // "0xbe9936fcfc50666f5425fde4a9decc59cef73b24", - // ], - // "uri": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", - // }, - // "owner": "0xbe9936fcfc50666f5425fde4a9decc59cef73b24", - // "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", - // "tokenURI": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", - // "type": "ERC721", - // } - // `); + expect(nft).toMatchInlineSnapshot(` + { + "chainId": 1, + "id": 1n, + "metadata": { + "attributes": [ + { + "trait_type": "face", + "value": "holographic beard", + }, + { + "trait_type": "hair", + "value": "white bucket cap", + }, + { + "trait_type": "body", + "value": "purple sweater with satchel", + }, + { + "trait_type": "background", + "value": "grey", + }, + { + "trait_type": "head", + "value": "gradient 2", + }, + ], + "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", + "image": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", + "image_url": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", + "name": "Doodle #1", + "owner_addresses": [ + "0xbe9936fcfc50666f5425fde4a9decc59cef73b24", + ], + "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", + }, + "owner": "0xbe9936fcfc50666f5425fde4a9decc59cef73b24", + "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", + "tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", + "type": "ERC721", + } + `); }); it("without owner", async () => { diff --git a/packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts b/packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts index f85e59cc53d..e4c437eccfa 100644 --- a/packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts +++ b/packages/thirdweb/src/extensions/erc721/read/getNFTs.test.ts @@ -14,195 +14,195 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => { expect(nfts.length).toBe(5); // TODO (insight): re-enable once insight fixes the client id caching issue - // expect(nfts).toMatchInlineSnapshot(` - // [ - // { - // "chainId": 1, - // "id": 0n, - // "metadata": { - // "attributes": [ - // { - // "trait_type": "face", - // "value": "mustache", - // }, - // { - // "trait_type": "hair", - // "value": "purple long", - // }, - // { - // "trait_type": "body", - // "value": "blue and yellow jacket", - // }, - // { - // "trait_type": "background", - // "value": "green", - // }, - // { - // "trait_type": "head", - // "value": "tan", - // }, - // ], - // "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", - // "image": "https://${clientId}.ipfscdn.io/ipfs/QmUEfFfwAh4wyB5UfHCVPUxis4j4Q4kJXtm5x5p3g1fVUn", - // "image_url": "https://${clientId}.ipfscdn.io/ipfs/QmUEfFfwAh4wyB5UfHCVPUxis4j4Q4kJXtm5x5p3g1fVUn", - // "name": "Doodle #0", - // "uri": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/0", - // }, - // "owner": null, - // "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", - // "tokenURI": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/0", - // "type": "ERC721", - // }, - // { - // "chainId": 1, - // "id": 1n, - // "metadata": { - // "attributes": [ - // { - // "trait_type": "face", - // "value": "holographic beard", - // }, - // { - // "trait_type": "hair", - // "value": "white bucket cap", - // }, - // { - // "trait_type": "body", - // "value": "purple sweater with satchel", - // }, - // { - // "trait_type": "background", - // "value": "grey", - // }, - // { - // "trait_type": "head", - // "value": "gradient 2", - // }, - // ], - // "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", - // "image": "https://${clientId}.ipfscdn.io/ipfs/QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", - // "image_url": "https://${clientId}.ipfscdn.io/ipfs/QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", - // "name": "Doodle #1", - // "uri": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", - // }, - // "owner": null, - // "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", - // "tokenURI": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", - // "type": "ERC721", - // }, - // { - // "chainId": 1, - // "id": 2n, - // "metadata": { - // "attributes": [ - // { - // "trait_type": "face", - // "value": "designer glasses", - // }, - // { - // "trait_type": "hair", - // "value": "poopie", - // }, - // { - // "trait_type": "body", - // "value": "blue fleece", - // }, - // { - // "trait_type": "background", - // "value": "yellow", - // }, - // { - // "trait_type": "head", - // "value": "purple", - // }, - // ], - // "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", - // "image": "https://${clientId}.ipfscdn.io/ipfs/QmbvZ2hbF3nEq5r3ijMEiSGssAmJvtyFwiejTAGHv74LR5", - // "image_url": "https://${clientId}.ipfscdn.io/ipfs/QmbvZ2hbF3nEq5r3ijMEiSGssAmJvtyFwiejTAGHv74LR5", - // "name": "Doodle #2", - // "uri": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/2", - // }, - // "owner": null, - // "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", - // "tokenURI": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/2", - // "type": "ERC721", - // }, - // { - // "chainId": 1, - // "id": 3n, - // "metadata": { - // "attributes": [ - // { - // "trait_type": "face", - // "value": "designer glasses", - // }, - // { - // "trait_type": "hair", - // "value": "holographic mohawk", - // }, - // { - // "trait_type": "body", - // "value": "pink fleece", - // }, - // { - // "trait_type": "background", - // "value": "gradient 1", - // }, - // { - // "trait_type": "head", - // "value": "pale", - // }, - // ], - // "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", - // "image": "https://${clientId}.ipfscdn.io/ipfs/QmVpwaCqLut3wqwB5KSQr2fGnbLuJt5e3LhNvzvcisewZB", - // "image_url": "https://${clientId}.ipfscdn.io/ipfs/QmVpwaCqLut3wqwB5KSQr2fGnbLuJt5e3LhNvzvcisewZB", - // "name": "Doodle #3", - // "uri": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/3", - // }, - // "owner": null, - // "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", - // "tokenURI": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/3", - // "type": "ERC721", - // }, - // { - // "chainId": 1, - // "id": 4n, - // "metadata": { - // "attributes": [ - // { - // "trait_type": "face", - // "value": "happy", - // }, - // { - // "trait_type": "hair", - // "value": "purple long", - // }, - // { - // "trait_type": "body", - // "value": "spotted hoodie", - // }, - // { - // "trait_type": "background", - // "value": "gradient 2", - // }, - // { - // "trait_type": "head", - // "value": "purple", - // }, - // ], - // "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", - // "image": "https://${clientId}.ipfscdn.io/ipfs/QmcyuFVLbfBmSeQ9ynu4dk67r97nB1abEekotuVuRGWedm", - // "image_url": "https://${clientId}.ipfscdn.io/ipfs/QmcyuFVLbfBmSeQ9ynu4dk67r97nB1abEekotuVuRGWedm", - // "name": "Doodle #4", - // "uri": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/4", - // }, - // "owner": null, - // "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", - // "tokenURI": "https://${clientId}.ipfscdn.io/ipfs/QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/4", - // "type": "ERC721", - // }, - // ] - // `); + expect(nfts).toMatchInlineSnapshot(` + [ + { + "chainId": 1, + "id": 0n, + "metadata": { + "attributes": [ + { + "trait_type": "face", + "value": "mustache", + }, + { + "trait_type": "hair", + "value": "purple long", + }, + { + "trait_type": "body", + "value": "blue and yellow jacket", + }, + { + "trait_type": "background", + "value": "green", + }, + { + "trait_type": "head", + "value": "tan", + }, + ], + "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", + "image": "ipfs://QmUEfFfwAh4wyB5UfHCVPUxis4j4Q4kJXtm5x5p3g1fVUn", + "image_url": "ipfs://QmUEfFfwAh4wyB5UfHCVPUxis4j4Q4kJXtm5x5p3g1fVUn", + "name": "Doodle #0", + "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/0", + }, + "owner": null, + "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", + "tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/0", + "type": "ERC721", + }, + { + "chainId": 1, + "id": 1n, + "metadata": { + "attributes": [ + { + "trait_type": "face", + "value": "holographic beard", + }, + { + "trait_type": "hair", + "value": "white bucket cap", + }, + { + "trait_type": "body", + "value": "purple sweater with satchel", + }, + { + "trait_type": "background", + "value": "grey", + }, + { + "trait_type": "head", + "value": "gradient 2", + }, + ], + "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", + "image": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", + "image_url": "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9", + "name": "Doodle #1", + "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", + }, + "owner": null, + "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", + "tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1", + "type": "ERC721", + }, + { + "chainId": 1, + "id": 2n, + "metadata": { + "attributes": [ + { + "trait_type": "face", + "value": "designer glasses", + }, + { + "trait_type": "hair", + "value": "poopie", + }, + { + "trait_type": "body", + "value": "blue fleece", + }, + { + "trait_type": "background", + "value": "yellow", + }, + { + "trait_type": "head", + "value": "purple", + }, + ], + "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", + "image": "ipfs://QmbvZ2hbF3nEq5r3ijMEiSGssAmJvtyFwiejTAGHv74LR5", + "image_url": "ipfs://QmbvZ2hbF3nEq5r3ijMEiSGssAmJvtyFwiejTAGHv74LR5", + "name": "Doodle #2", + "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/2", + }, + "owner": null, + "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", + "tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/2", + "type": "ERC721", + }, + { + "chainId": 1, + "id": 3n, + "metadata": { + "attributes": [ + { + "trait_type": "face", + "value": "designer glasses", + }, + { + "trait_type": "hair", + "value": "holographic mohawk", + }, + { + "trait_type": "body", + "value": "pink fleece", + }, + { + "trait_type": "background", + "value": "gradient 1", + }, + { + "trait_type": "head", + "value": "pale", + }, + ], + "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", + "image": "ipfs://QmVpwaCqLut3wqwB5KSQr2fGnbLuJt5e3LhNvzvcisewZB", + "image_url": "ipfs://QmVpwaCqLut3wqwB5KSQr2fGnbLuJt5e3LhNvzvcisewZB", + "name": "Doodle #3", + "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/3", + }, + "owner": null, + "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", + "tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/3", + "type": "ERC721", + }, + { + "chainId": 1, + "id": 4n, + "metadata": { + "attributes": [ + { + "trait_type": "face", + "value": "happy", + }, + { + "trait_type": "hair", + "value": "purple long", + }, + { + "trait_type": "body", + "value": "spotted hoodie", + }, + { + "trait_type": "background", + "value": "gradient 2", + }, + { + "trait_type": "head", + "value": "purple", + }, + ], + "description": "A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.", + "image": "ipfs://QmcyuFVLbfBmSeQ9ynu4dk67r97nB1abEekotuVuRGWedm", + "image_url": "ipfs://QmcyuFVLbfBmSeQ9ynu4dk67r97nB1abEekotuVuRGWedm", + "name": "Doodle #4", + "uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/4", + }, + "owner": null, + "tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", + "tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/4", + "type": "ERC721", + }, + ] + `); }); it("should throw error if totalSupply and nextTokenIdToMint are not supported", async () => { diff --git a/packages/thirdweb/src/insight/get-nfts.ts b/packages/thirdweb/src/insight/get-nfts.ts index a6f7f622588..e6579b69aaa 100644 --- a/packages/thirdweb/src/insight/get-nfts.ts +++ b/packages/thirdweb/src/insight/get-nfts.ts @@ -9,6 +9,8 @@ import type { Chain } from "../chains/types.js"; import type { ThirdwebClient } from "../client/client.js"; import type { NFT } from "../utils/nft/parseNft.js"; +import { getCachedChain } from "../chains/utils.js"; +import { getContract } from "../contract/contract.js"; type OwnedNFT = GetV1NftsResponse["data"][number]; type ContractNFT = GetV1NftsByContractAddressResponse["data"][number]; @@ -76,6 +78,7 @@ export async function getOwnedNFTs(args: { const transformedNfts = await transformNFTModel( result.data?.data ?? [], + client, ownerAddress, ); return transformedNfts.map((nft) => ({ @@ -156,7 +159,7 @@ export async function getContractNFTs(args: { ); } - return transformNFTModel(result.data?.data ?? []); + return transformNFTModel(result.data?.data ?? [], client); } /** @@ -233,64 +236,125 @@ export async function getNFT(args: { ); } - const transformedNfts = await transformNFTModel(result.data?.data ?? []); + const transformedNfts = await transformNFTModel( + result.data?.data ?? [], + client, + ); return transformedNfts?.[0]; } async function transformNFTModel( nfts: (ContractNFT | OwnedNFT)[], + client: ThirdwebClient, ownerAddress?: string, ): Promise<(NFT & { quantityOwned?: bigint })[]> { - const { parseNFT } = await import("../utils/nft/parseNft.js"); + const [{ parseNFT }, { totalSupply }] = await Promise.all([ + import("../utils/nft/parseNft.js"), + import("../extensions/erc1155/__generated__/IERC1155/read/totalSupply.js"), + ]); - return nfts.map((nft) => { - let parsedNft: NFT; - const { - contract, - extra_metadata, - collection, - metadata_url, - chain_id, - token_id, - status, - balance, - token_type, - ...rest - } = nft; - const metadata = { - uri: nft.metadata_url ?? "", - image: nft.image_url, - attributes: nft.extra_metadata?.attributes ?? undefined, - ...rest, - }; + return await Promise.all( + nfts.map(async (nft) => { + let parsedNft: NFT; + const { + contract, + extra_metadata, + collection, + metadata_url, + chain_id, + token_id, + status, + balance, + token_type, + ...rest + } = nft; + const metadata = replaceIPFSGatewayRecursively({ + uri: nft.metadata_url ?? "", + image: nft.image_url, + attributes: nft.extra_metadata?.attributes ?? undefined, + ...rest, + }); - const owner_addresses = ownerAddress - ? [ownerAddress] - : "owner_addresses" in nft - ? nft.owner_addresses - : undefined; + // replace the ipfs gateway with the ipfs gateway from the client recusively for each key in the metadata object - if (contract?.type === "erc1155") { - parsedNft = parseNFT(metadata, { - tokenId: BigInt(token_id), - tokenUri: metadata_url ?? "", - type: "ERC1155", - owner: owner_addresses?.[0], - tokenAddress: contract?.address ?? "", - chainId: contract?.chain_id ?? 0, - supply: balance ? BigInt(balance) : 0n, // TODO (insight): this is wrong, needs to be added in the API - }); + const owner_addresses = ownerAddress + ? [ownerAddress] + : "owner_addresses" in nft + ? nft.owner_addresses + : undefined; + + if (contract?.type === "erc1155") { + // TODO (insight): this needs to be added in the API + const supply = await totalSupply({ + contract: getContract({ + address: contract.address, + chain: getCachedChain(contract.chain_id), + client: client, + }), + id: BigInt(token_id), + }); + + parsedNft = parseNFT(metadata, { + tokenId: BigInt(token_id), + tokenUri: replaceIPFSGateway(metadata_url) ?? "", + type: "ERC1155", + owner: owner_addresses?.[0], + tokenAddress: contract?.address ?? "", + chainId: contract?.chain_id ?? 0, + supply: supply, + }); + } else { + parsedNft = parseNFT(metadata, { + tokenId: BigInt(token_id), + type: "ERC721", + owner: owner_addresses?.[0], + tokenUri: replaceIPFSGateway(metadata_url) ?? "", + tokenAddress: contract?.address ?? "", + chainId: contract?.chain_id ?? 0, + }); + } + + return parsedNft; + }), + ); +} + +// biome-ignore lint/suspicious/noExplicitAny: this should be fixed in the API +function replaceIPFSGatewayRecursively(obj: any) { + if (typeof obj !== "object" || obj === null) { + return obj; + } + for (const key in obj) { + if (typeof obj[key] === "string") { + obj[key] = replaceIPFSGateway(obj[key]); } else { - parsedNft = parseNFT(metadata, { - tokenId: BigInt(token_id), - type: "ERC721", - owner: owner_addresses?.[0], - tokenUri: metadata_url ?? "", - tokenAddress: contract?.address ?? "", - chainId: contract?.chain_id ?? 0, - }); + replaceIPFSGatewayRecursively(obj[key]); } + } + return obj; +} - return parsedNft; - }); +function replaceIPFSGateway(url?: string) { + if (!url || typeof url !== "string") { + return url; + } + try { + const parsedUrl = new URL(url); + if (parsedUrl.host.endsWith(".ipfscdn.io")) { + const paths = parsedUrl.pathname.split("/"); + const index = paths.findIndex((path) => path === "ipfs"); + if (index === -1) { + return url; + } + const ipfsHash = paths.slice(index + 1).join("/"); + if (ipfsHash) { + return `ipfs://${ipfsHash}`; + } + return url; + } + } catch { + // If the URL is invalid, return it as is + return url; + } + return url; }