[SDK] Add ERC1155 getOwnedTokenIds (#4345)

## Problem solved

Let's say I need a list of owned tokenids for rendering a list of <NFT.Media />. Using getOwnedNFTs is a waste of requests, so I split the code from the current getOwnedNFTs to make a new function

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a new function `getOwnedTokenIds` to retrieve ERC1155 tokenIds and their balances for a wallet address, along with related tests.

### Detailed summary
- Added `getOwnedTokenIds` function to retrieve ERC1155 tokenIds and balances
- Updated `getOwnedNFTs` to use `getOwnedTokenIds`
- Added tests for `getOwnedTokenIds`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
kien-ngo
2024-09-05 09:58:46 +00:00
parent e6f8b3edad
commit fa6809d500
5 changed files with 156 additions and 64 deletions
@@ -21,6 +21,10 @@ export {
nextTokenIdToMint,
isNextTokenIdToMintSupported,
} from "../../extensions/erc1155/__generated__/IERC1155Enumerable/read/nextTokenIdToMint.js";
export {
getOwnedTokenIds,
type GetOwnedTokenIdsParams,
} from "../../extensions/erc1155/read/getOwnedTokenIds.js";
export {
uri,
/**
@@ -1,31 +1,16 @@
import type { Address } from "abitype";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import type { NFT } from "../../../utils/nft/parseNft.js";
import { balanceOfBatch } from "../__generated__/IERC1155/read/balanceOfBatch.js";
import { nextTokenIdToMint } from "../__generated__/IERC1155Enumerable/read/nextTokenIdToMint.js";
import { nextTokenId } from "../__generated__/Zora1155/read/nextTokenId.js";
import { getNFT } from "./getNFT.js";
const DEFAULT_QUERY_ALL_COUNT = 100;
import {
type GetOwnedTokenIdsParams,
getOwnedTokenIds,
} from "./getOwnedTokenIds.js";
/**
* Parameters for retrieving NFTs.
* @extension ERC1155
*/
export type GetOwnedNFTsParams = {
/**
* Which tokenId to start at.
*/
start?: number;
/**
* The number of NFTs to retrieve.
*/
count?: number;
/**
* The address of the wallet to get the NFTs of.
*/
address: string;
};
export type GetOwnedNFTsParams = GetOwnedTokenIdsParams;
/**
* Retrieves the owned ERC1155 NFTs for a given wallet address.
@@ -46,52 +31,10 @@ export type GetOwnedNFTsParams = {
export async function getOwnedNFTs(
options: BaseTransactionOptions<GetOwnedNFTsParams>,
): Promise<(NFT & { quantityOwned: bigint })[]> {
const maxId = await Promise.allSettled([
nextTokenIdToMint(options),
nextTokenId(options),
]).then(([_nextToMint, _next]) => {
if (_nextToMint.status === "fulfilled") {
return _nextToMint.value;
}
if (_next.status === "fulfilled") {
return _next.value;
}
throw Error("Contract doesn't have required extension");
});
// approach is naieve, likely can be improved
const owners: Address[] = [];
const tokenIds: bigint[] = [];
for (let i = 0n; i < maxId; i++) {
owners.push(options.address);
tokenIds.push(i);
}
const balances = await balanceOfBatch({
...options,
owners,
tokenIds,
});
let ownedBalances = balances
.map((b, i) => {
return {
tokenId: i,
balance: b,
};
})
.filter((b) => b.balance > 0);
if (options.start || options.count) {
const start = options?.start || 0;
const count = options?.count || DEFAULT_QUERY_ALL_COUNT;
ownedBalances = ownedBalances.slice(start, start + count);
}
const ownedBalances = await getOwnedTokenIds(options);
const nfts = await Promise.all(
ownedBalances.map((ob) =>
getNFT({ ...options, tokenId: BigInt(ob.tokenId) }),
),
ownedBalances.map((ob) => getNFT({ ...options, tokenId: ob.tokenId })),
);
return nfts.map((nft, index) => ({
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CONTRACT_URI } from "~test/ipfs-uris.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { getContract } from "../../../contract/contract.js";
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
import { deployERC1155Contract } from "../../prebuilts/deploy-erc1155.js";
import { mintTo } from "../write/mintTo.js";
import { getOwnedTokenIds } from "./getOwnedTokenIds.js";
const account = TEST_ACCOUNT_A;
const client = TEST_CLIENT;
const chain = ANVIL_CHAIN;
describe.runIf(process.env.TW_SECRET_KEY)("ERC1155 getOwnedTokenIds", () => {
it("should fetch owned tokenIds", async () => {
const address = await deployERC1155Contract({
type: "TokenERC1155",
chain,
client,
account,
params: {
name: "edition",
contractURI: TEST_CONTRACT_URI,
},
});
const contract = getContract({
address,
chain,
client,
});
const transaction = mintTo({
contract,
nft: { name: "token 0" },
to: account.address,
supply: 20n,
});
await sendAndConfirmTransaction({ transaction, account });
const ownedTokenIds = await getOwnedTokenIds({
contract,
address: account.address,
});
expect(ownedTokenIds).toStrictEqual([{ tokenId: 0n, balance: 20n }]);
});
});
@@ -0,0 +1,90 @@
import type { Address } from "abitype";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { balanceOfBatch } from "../__generated__/IERC1155/read/balanceOfBatch.js";
import { nextTokenIdToMint } from "../__generated__/IERC1155Enumerable/read/nextTokenIdToMint.js";
import { nextTokenId } from "../__generated__/Zora1155/read/nextTokenId.js";
const DEFAULT_QUERY_ALL_COUNT = 100;
/**
* Parameters for retrieving owned tokenIds of an ERC1155 contract.
* @extension ERC1155
*/
export type GetOwnedTokenIdsParams = {
/**
* Which tokenId to start at.
*/
start?: number;
/**
* The number of NFTs to retrieve.
*/
count?: number;
/**
* The address of the wallet to get the NFTs of.
*/
address: string;
};
/**
* Retrieves the owned ERC1155 tokenIds & the owned balance of each tokenId for a given wallet address.
* @param options - The transaction options and parameters.
* @returns A promise that resolves to an array of ERC1155 NFTs owned by the wallet address, along with the quantity owned.
* @extension ERC1155
* @example
* ```ts
* import { getOwnedNFTs } from "thirdweb/extensions/erc1155";
* const ownedTokenIds = await getOwnedTokenIds({
* contract,
* start: 0,
* count: 10,
* address: "0x123...",
* });
* ```
*/
export async function getOwnedTokenIds(
options: BaseTransactionOptions<GetOwnedTokenIdsParams>,
): Promise<{ tokenId: bigint; balance: bigint }[]> {
const maxId = await Promise.allSettled([
nextTokenIdToMint(options),
nextTokenId(options),
]).then(([_nextToMint, _next]) => {
if (_nextToMint.status === "fulfilled") {
return _nextToMint.value;
}
if (_next.status === "fulfilled") {
return _next.value;
}
throw Error("Contract doesn't have required extension");
});
// approach is naieve, likely can be improved
const owners: Address[] = [];
const tokenIds: bigint[] = [];
for (let i = 0n; i < maxId; i++) {
owners.push(options.address);
tokenIds.push(i);
}
const balances = await balanceOfBatch({
...options,
owners,
tokenIds,
});
let ownedBalances = balances
.map((b, i) => {
return {
tokenId: BigInt(i),
balance: b,
};
})
.filter((b) => b.balance > 0);
if (options.start || options.count) {
const start = options?.start || 0;
const count = options?.count || DEFAULT_QUERY_ALL_COUNT;
ownedBalances = ownedBalances.slice(start, start + count);
}
return ownedBalances;
}