From 61118091708ea77343bfb560f7b73a12d3cff879 Mon Sep 17 00:00:00 2001 From: Greg Nazario Date: Thu, 14 Nov 2024 16:52:40 -0500 Subject: [PATCH] [account] Fix pagination on AccountModules and AccountResources Resolves https://github.com/aptos-labs/aptos-ts-sdk/issues/576 --- CHANGELOG.md | 1 + src/api/account.ts | 4 ++-- src/client/get.ts | 42 +++++++++++++++++++++++++++++++++++ src/internal/account.ts | 14 ++++++------ tests/e2e/api/account.test.ts | 27 +++++++++++++++++++++- 5 files changed, 78 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab226399f..fe75503a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. This changelog is written by hand for now. It adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Unreleased +- [`Fix`] Fixes pagination for GetAccountModules and GetAccountResources. Also, adds more appropriate documentation on offset. # 1.33.0 (2024-11-13) - Allow optional provision of public keys in transaction simulation diff --git a/src/api/account.ts b/src/api/account.ts index 5740382d7..150d6e1b0 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -104,7 +104,7 @@ export class Account { * This function may call the API multiple times to auto paginate through results. * * @param args.accountAddress - The Aptos account address to query modules for. - * @param args.options.offset - The number of modules to start returning results from. + * @param args.options.offset - The cursor to start returning results from. Note, this is obfuscated and is not an index. * @param args.options.limit - The maximum number of results to return. * @param args.options.ledgerVersion - The ledger version to query; if not provided, it retrieves the latest version. * @@ -223,7 +223,7 @@ export class Account { * This function may call the API multiple times to auto paginate through results. * * @param args.accountAddress - The Aptos account address to query resources for. - * @param args.options.offset - The number of resources to start returning results from. + * @param args.options.offset - The cursor to start returning results from. Note, this is obfuscated and is not an index. * @param args.options.limit - The maximum number of results to return. * @param args.options.ledgerVersion - The ledger version to query; if not provided, it will get the latest version. * @returns Account resources. diff --git a/src/client/get.ts b/src/client/get.ts index e3fd9c007..388a8ae97 100644 --- a/src/client/get.ts +++ b/src/client/get.ts @@ -159,3 +159,45 @@ export async function paginateWithCursor, Res ex } while (cursor !== null && cursor !== undefined); return out as Res; } + +/// This function is a helper for paginating using a function wrapping an API using offset instead of start +export async function paginateWithObfuscatedCursor, Res extends Array<{}>>( + options: GetAptosRequestOptions, +): Promise { + const out: any[] = []; + let cursor: string | undefined; + const requestParams = options.params as { offset?: string; limit?: number }; + const totalLimit = requestParams.limit; + do { + // eslint-disable-next-line no-await-in-loop + const response = await get({ + type: AptosApiType.FULLNODE, + aptosConfig: options.aptosConfig, + originMethod: options.originMethod, + path: options.path, + params: requestParams, + overrides: options.overrides, + }); + /** + * the cursor is a "state key" from the API perspective. Client + * should not need to "care" what it represents but just use it + * to query the next chunk of data. + */ + cursor = response.headers["x-aptos-cursor"]; + // Now that we have the cursor (if any), we remove the headers before + // adding these to the output of this function. + delete response.headers; + out.push(...response.data); + requestParams.offset = cursor; + + // Re-evaluate length + if (totalLimit !== undefined) { + const newLimit = totalLimit - out.length; + if (newLimit <= 0) { + break; + } + requestParams.limit = newLimit; + } + } while (cursor !== null && cursor !== undefined); + return out as Res; +} diff --git a/src/internal/account.ts b/src/internal/account.ts index 9b47ee4ee..0293453e9 100644 --- a/src/internal/account.ts +++ b/src/internal/account.ts @@ -8,7 +8,7 @@ * account namespace and without having a dependency cycle error. */ import { AptosConfig } from "../api/aptosConfig"; -import { getAptosFullNode, paginateWithCursor } from "../client"; +import { getAptosFullNode, paginateWithCursor, paginateWithObfuscatedCursor } from "../client"; import { AccountData, GetAccountCoinsDataResponse, @@ -85,7 +85,7 @@ export async function getInfo(args: { * @param args.accountAddress - The address of the account whose modules are to be retrieved. * @param args.options - Optional parameters for pagination and ledger version. * @param args.options.limit - The maximum number of modules to retrieve (default is 1000). - * @param args.options.offset - The starting point for pagination. + * @param args.options.offset - The starting point for pagination. Note, this is obfuscated and is not an index. * @param args.options.ledgerVersion - The specific ledger version to query. */ export async function getModules(args: { @@ -94,13 +94,13 @@ export async function getModules(args: { options?: PaginationArgs & LedgerVersionArg; }): Promise { const { aptosConfig, accountAddress, options } = args; - return paginateWithCursor<{}, MoveModuleBytecode[]>({ + return paginateWithObfuscatedCursor<{}, MoveModuleBytecode[]>({ aptosConfig, originMethod: "getModules", path: `accounts/${AccountAddress.from(accountAddress).toString()}/modules`, params: { ledger_version: options?.ledgerVersion, - start: options?.offset, + offset: options?.offset, limit: options?.limit ?? 1000, }, }); @@ -196,7 +196,7 @@ export async function getTransactions(args: { * @param args.aptosConfig - The configuration settings for Aptos. * @param args.accountAddress - The address of the account to fetch resources for. * @param args.options - Optional pagination and ledger version parameters. - * @param args.options.offset - The starting point for pagination. + * @param args.options.offset - The starting point for pagination. Note, this is obfuscated and is not an index. * @param args.options.limit - The maximum number of resources to retrieve (default is 999). * @param args.options.ledgerVersion - The specific ledger version to query. */ @@ -206,13 +206,13 @@ export async function getResources(args: { options?: PaginationArgs & LedgerVersionArg; }): Promise { const { aptosConfig, accountAddress, options } = args; - return paginateWithCursor<{}, MoveResource[]>({ + return paginateWithObfuscatedCursor<{}, MoveResource[]>({ aptosConfig, originMethod: "getResources", path: `accounts/${AccountAddress.from(accountAddress).toString()}/resources`, params: { ledger_version: options?.ledgerVersion, - start: options?.offset, + offset: options?.offset, limit: options?.limit ?? 999, }, }); diff --git a/tests/e2e/api/account.test.ts b/tests/e2e/api/account.test.ts index 842bb3e09..144975c3e 100644 --- a/tests/e2e/api/account.test.ts +++ b/tests/e2e/api/account.test.ts @@ -30,12 +30,24 @@ describe("account api", () => { }); test("it fetches account modules", async () => { + const { aptos } = getAptosClient(); + const data = await aptos.getAccountModules({ + accountAddress: "0x1", + }); + expect(data.length).toBeGreaterThan(0); + }); + + test("it fetches account modules with pagination", async () => { const config = new AptosConfig({ network: Network.LOCAL }); const aptos = new Aptos(config); const data = await aptos.getAccountModules({ accountAddress: "0x1", + options: { + offset: 1, + limit: 1, + }, }); - expect(data.length).toBeGreaterThan(0); + expect(data.length).toEqual(1); }); test("it fetches an account module", async () => { @@ -57,6 +69,19 @@ describe("account api", () => { expect(data.length).toBeGreaterThan(0); }); + test("it fetches account resources with pagination", async () => { + const config = new AptosConfig({ network: Network.LOCAL }); + const aptos = new Aptos(config); + const data = await aptos.getAccountResources({ + accountAddress: "0x1", + options: { + offset: 1, + limit: 1, + }, + }); + expect(data.length).toEqual(1); + }); + test("it fetches an account resource without a type", async () => { const config = new AptosConfig({ network: Network.LOCAL }); const aptos = new Aptos(config);