Skip to content

Commit e9f1cfd

Browse files
authored
Add pagination for getNftsForCollection (#6)
1 parent 84a8ef2 commit e9f1cfd

File tree

10 files changed

+301
-20
lines changed

10 files changed

+301
-20
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ const alchemy = initializeAlchemy(settings);
3232
The SDK's modular approach exports all functions at the top-level to reduce bundle size. However,
3333
this can make it harder to discover the full API surface. If you want your IDE to find all functions, you can import
3434
the entire SDK:
35+
3536
```ts
3637
import * as alchemySdk from 'exploring-pioneer';
3738

3839
const alchemy = alchemySdk.initializeAlchemy();
39-
alchemySdk.getNfts(alchemy, {owner: '0x123'});
40+
alchemySdk.getNfts(alchemy, { owner: '0x123' });
4041
```
4142

4243
## SDK Structure

src/api/nft-api.ts

+69-8
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function getNftMetadata(
6363
tokenType?: NftTokenType
6464
): Promise<Nft> {
6565
let response;
66-
let contractAddress = '';
66+
let contractAddress: string;
6767
if (typeof contractAddressOrBaseNft === 'string') {
6868
validateContractAddress(contractAddressOrBaseNft);
6969
contractAddress = contractAddressOrBaseNft;
@@ -95,7 +95,7 @@ export async function getNftMetadata(
9595
}
9696

9797
/**
98-
* - Fetches all NFTs for a given owner and yields them in an async iterable.
98+
* Fetches all NFTs for a given owner and yields them in an async iterable.
9999
*
100100
* This method returns the base NFTs that omit the associated metadata and pages
101101
* through all page keys until all NFTs have been fetched.
@@ -126,10 +126,16 @@ export async function* getNftsPaginated(
126126
params: GetNftsParams | GetBaseNftsParams
127127
): AsyncIterable<OwnedBaseNft | OwnedNft> {
128128
const withMetadata = params.withMetadata ?? true;
129-
for await (const response of paginateEndpoint(alchemy, 'getNFTs', 'pageKey', {
130-
...params,
131-
withMetadata
132-
})) {
129+
for await (const response of paginateEndpoint(
130+
alchemy,
131+
'getNFTs',
132+
'pageKey',
133+
'pageKey',
134+
{
135+
...params,
136+
withMetadata
137+
}
138+
)) {
133139
for (const ownedNft of response.ownedNfts as
134140
| RawOwnedNft[]
135141
| RawOwnedBaseNft[]) {
@@ -199,7 +205,6 @@ export async function getNfts(
199205
* @param params The parameters to use for the request.
200206
* @beta
201207
*/
202-
// TODO: Add pagination for this endpoint.
203208
export async function getNftsForCollection(
204209
alchemy: Alchemy,
205210
params: GetBaseNftsForCollectionParams
@@ -216,7 +221,6 @@ export async function getNftsForCollection(
216221
* {@link CollectionNftsResponse} response.
217222
* @beta
218223
*/
219-
// TODO: add pagination for this endpoint.
220224
export async function getNftsForCollection(
221225
alchemy: Alchemy,
222226
params: GetNftsForCollectionParams
@@ -287,6 +291,63 @@ export function getOwnersForToken(
287291
}
288292
}
289293

294+
/**
295+
* Fetches all base NFTs for a given contract address and yields them in an
296+
* async iterable.
297+
*
298+
* This method returns the base NFTs that omit the associated metadata and pages
299+
* through all page keys until all NFTs have been fetched. To get all NFTs with
300+
* their associated metadata, use {@link GetNftsForCollectionParams}.
301+
*
302+
* @param alchemy The Alchemy SDK instance.
303+
* @param params The parameters to use for the request.
304+
* @beta
305+
*/
306+
export function getNftsForCollectionPaginated(
307+
alchemy: Alchemy,
308+
params: GetBaseNftsForCollectionParams
309+
): AsyncIterable<BaseNft>;
310+
311+
/**
312+
* Fetches all NFTs for a given contract address and yields them in an async iterable.
313+
*
314+
* This method returns the full NFTs in the contract and pages through all page
315+
* keys until all NFTs have been fetched. To get all NFTs without their
316+
* associated metadata, use {@link GetBaseNftsForCollectionParams}.
317+
*
318+
* @param alchemy The Alchemy SDK instance.
319+
* @param params The parameters to use for the request.
320+
* @beta
321+
*/
322+
export function getNftsForCollectionPaginated(
323+
alchemy: Alchemy,
324+
params: GetNftsForCollectionParams
325+
): AsyncIterable<Nft>;
326+
export async function* getNftsForCollectionPaginated(
327+
alchemy: Alchemy,
328+
params: GetBaseNftsForCollectionParams | GetNftsForCollectionParams
329+
): AsyncIterable<BaseNft | Nft> {
330+
const withMetadata =
331+
params.withMetadata !== undefined ? params.withMetadata : true;
332+
for await (const response of paginateEndpoint(
333+
alchemy,
334+
'getNFTsForCollection',
335+
'startToken',
336+
'nextToken',
337+
{
338+
contractAddress: params.contractAddress,
339+
startToken: params.pageKey,
340+
withMetadata
341+
}
342+
)) {
343+
for (const nft of response.nfts as
344+
| RawCollectionBaseNft[]
345+
| RawCollectionNft[]) {
346+
yield nftFromGetNftCollectionResponse(nft, params.contractAddress);
347+
}
348+
}
349+
}
350+
290351
/**
291352
* Helper method to convert a NFT response received from Alchemy backend to an
292353
* SDK NFT type.

src/api/nft.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export class BaseNft {
2727
static fromResponse(ownedNft: RawBaseNft, contractAddress: string): BaseNft {
2828
return new BaseNft(
2929
contractAddress,
30-
ownedNft.id.tokenId,
30+
// We have to normalize the token id here since the backend sometimes
31+
// returns the token ID as a hex string and sometimes as an integer.
32+
normalizeTokenIdToHex(ownedNft.id.tokenId),
3133
ownedNft.id.tokenMetadata?.tokenType ?? NftTokenType.UNKNOWN
3234
);
3335
}

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {
1010
getNfts,
1111
getNftsPaginated,
1212
getNftsForCollection,
13+
getNftsForCollectionPaginated,
1314
getOwnersForToken
1415
} from './api/nft-api';
1516

src/internal/dispatch.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ function isRetryableHttpError(err: AxiosError): boolean {
7979
* @internal
8080
*/
8181
export async function* paginateEndpoint<
82-
PageKey extends string,
83-
Req extends Partial<Record<PageKey, string>>,
84-
Res extends Partial<Record<string, any> & Record<PageKey, string>>
82+
ReqPageKey extends string,
83+
ResPageKey extends string,
84+
Req extends Partial<Record<string, any> & Record<ReqPageKey, string>>,
85+
Res extends Partial<Record<string, any> & Record<ResPageKey, string>>
8586
>(
8687
alchemy: Alchemy,
8788
methodName: string,
88-
pageKey: PageKey,
89+
reqPageKey: ReqPageKey,
90+
resPageKey: ResPageKey,
8991
params: Req
9092
): AsyncIterable<Res> {
9193
let hasNext = true;
@@ -97,9 +99,8 @@ export async function* paginateEndpoint<
9799
requestParams
98100
);
99101
yield response;
100-
if (response[pageKey] !== undefined) {
101-
/* eslint-disable @typescript-eslint/no-explicit-any */
102-
requestParams[pageKey] = response[pageKey] as any;
102+
if (response[resPageKey] !== undefined) {
103+
requestParams[reqPageKey] = response[resPageKey] as any;
103104
} else {
104105
hasNext = false;
105106
}

src/types/types.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ export interface RawContract {
282282
}
283283

284284
/**
285-
* Parameters object for the {@link getNftsForCollection} function.
285+
* Parameters object for the {@link getNftsForCollection} and
286+
* {@link getNftsForCollectionPaginated} functions.
286287
*
287288
* This interface is used to fetch NFTs with their associated metadata. To get
288289
* Nfts without their associated metadata, use
@@ -307,7 +308,8 @@ export interface GetNftsForCollectionParams {
307308
}
308309

309310
/**
310-
* Parameters object for the {@link getNftsForCollection} function.
311+
* Parameters object for the {@link getNftsForCollection} and
312+
* {@link getNftsForCollectionPaginated} functions.
311313
*
312314
* This interface is used to fetch NFTs without their associated metadata. To
313315
* get Nfts with their associated metadata, use

src/util/util.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export function formatBlock(block: string | number): string {
99
return block.toString();
1010
}
1111

12+
// TODO: Add loglevel options to avoid always logging everything
1213
export function logger(methodName: string, message: string) {
1314
console.log(`[${methodName}]: ${message}`);
1415
}

test/integration.test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
NftTokenType
99
} from '../src';
1010
import { Alchemy } from '../src/api/alchemy';
11+
import { getNftsForCollectionPaginated } from '../src/api/nft-api';
1112

1213
/**
1314
* Temporary test
@@ -117,4 +118,22 @@ describe('E2E integration tests', () => {
117118
}
118119
console.log('done', allNfts.length, allNfts);
119120
});
121+
122+
it('getNftsForCollectionPaginated', async () => {
123+
jest.setTimeout(15000);
124+
console.log('lets paginate');
125+
const allNfts = [];
126+
let totalCount = 0;
127+
for await (const nft of getNftsForCollectionPaginated(alchemy, {
128+
contractAddress,
129+
withMetadata: false
130+
})) {
131+
if (totalCount === 150) {
132+
break;
133+
}
134+
allNfts.push(nft);
135+
totalCount += 1;
136+
}
137+
console.log('done', allNfts.length, allNfts[100], totalCount);
138+
});
120139
});

0 commit comments

Comments
 (0)