分页读取
当查询结果可能包含大量数据时(如某地址拥有数百个 NFT),Sui API 使用基于游标(cursor)的分页机制。本节将介绍如何正确处理分页,获取完整的数据集。
分页机制
Sui 的分页 API 返回三个关键字段:
{
data: [...], // 当前页的数据
nextCursor: "...", // 下一页的游标(null 表示无更多数据)
hasNextPage: true // 是否有下一页
}
基本分页查询
getOwnedObjects 分页
import { SuiGrpcClient } from "@mysten/sui/grpc";
const client = new SuiGrpcClient({
network: "testnet",
baseUrl: "https://fullnode.testnet.sui.io:443",
});
// 获取第一页
const firstPage = await client.core.listOwnedObjects({
owner: userAddress,
include: { type: true },
limit: 50, // 每页最多 50 条
});
console.log(`Page 1: ${firstPage.data.length} objects`);
console.log(`Has next page: ${firstPage.hasNextPage}`);
// 获取第二页
if (firstPage.hasNextPage && firstPage.nextCursor) {
const secondPage = await client.core.listOwnedObjects({
owner: userAddress,
include: { type: true },
limit: 50,
cursor: firstPage.nextCursor,
});
console.log(`Page 2: ${secondPage.data.length} objects`);
}
获取所有数据
循环分页
最常见的模式——循环获取所有页面:
async function getAllOwnedObjects(
client: SuiGrpcClient,
owner: string,
): Promise<any[]> {
const allObjects: any[] = [];
let cursor: string | null | undefined = undefined;
let hasNextPage = true;
while (hasNextPage) {
const page = await client.core.listOwnedObjects({
owner,
include: { content: true, type: true },
limit: 50,
cursor,
});
allObjects.push(...page.data);
hasNextPage = page.hasNextPage;
cursor = page.nextCursor;
}
return allObjects;
}
// 使用
const objects = await getAllOwnedObjects(client, userAddress);
console.log(`Total objects: ${objects.length}`);
带类型过滤的分页
async function getAllHeroes(
client: SuiGrpcClient,
owner: string,
packageId: string,
): Promise<any[]> {
const allHeroes: any[] = [];
let cursor: string | null | undefined = undefined;
let hasNextPage = true;
while (hasNextPage) {
const page = await client.core.listOwnedObjects({
owner,
filter: {
StructType: `${packageId}::hero::Hero`,
},
include: { content: true, display: true },
limit: 50,
cursor,
});
allHeroes.push(...page.data);
hasNextPage = page.hasNextPage;
cursor = page.nextCursor;
}
return allHeroes;
}
getDynamicFields 分页
动态字段查询同样支持分页:
async function getAllDynamicFields(
client: SuiGrpcClient,
parentId: string,
): Promise<any[]> {
const allFields: any[] = [];
let cursor: string | null | undefined = undefined;
let hasNextPage = true;
while (hasNextPage) {
const page = await client.core.listDynamicFields({
parentId,
limit: 50,
cursor,
});
allFields.push(...page.data);
hasNextPage = page.hasNextPage;
cursor = page.nextCursor;
}
return allFields;
}
查询交易记录分页
async function getTransactionHistory(
client: SuiGrpcClient,
address: string,
maxResults: number = 100,
): Promise<any[]> {
const transactions: any[] = [];
let cursor: string | null | undefined = undefined;
let hasNextPage = true;
while (hasNextPage && transactions.length < maxResults) {
const page = await client.core.queryTransactions({
filter: {
FromAddress: address,
},
include: { effects: true, events: true },
limit: Math.min(50, maxResults - transactions.length),
cursor,
order: "descending",
});
transactions.push(...page.data);
hasNextPage = page.hasNextPage;
cursor = page.nextCursor;
}
return transactions;
}
通用分页工具函数
创建一个可复用的分页工具:
interface PaginatedResult<T> {
data: T[];
nextCursor: string | null | undefined;
hasNextPage: boolean;
}
async function fetchAllPages<T>(
fetcher: (cursor?: string | null) => Promise<PaginatedResult<T>>,
maxItems?: number,
): Promise<T[]> {
const allItems: T[] = [];
let cursor: string | null | undefined = undefined;
let hasNextPage = true;
while (hasNextPage) {
if (maxItems && allItems.length >= maxItems) break;
const page = await fetcher(cursor);
allItems.push(...page.data);
hasNextPage = page.hasNextPage;
cursor = page.nextCursor;
}
return maxItems ? allItems.slice(0, maxItems) : allItems;
}
// 使用
const allObjects = await fetchAllPages((cursor) =>
client.core.listOwnedObjects({
owner: userAddress,
include: { type: true },
limit: 50,
cursor: cursor ?? undefined,
})
);
性能优化
并行获取详情
列表查询后需要获取详情时,使用 multiGetObjects 替代循环:
async function getOwnedHeroesWithDetails(
client: SuiGrpcClient,
owner: string,
packageId: string,
): Promise<any[]> {
// 步骤 1: 获取所有 Hero ID
const ownedObjects = await fetchAllPages((cursor) =>
client.core.listOwnedObjects({
owner,
filter: { StructType: `${packageId}::hero::Hero` },
limit: 50,
cursor: cursor ?? undefined,
})
);
const heroIds = ownedObjects
.map((obj) => obj.data?.objectId)
.filter(Boolean) as string[];
if (heroIds.length === 0) return [];
// 步骤 2: 批量获取详情(每批 50 个)
const batchSize = 50;
const allDetails: any[] = [];
for (let i = 0; i < heroIds.length; i += batchSize) {
const batch = heroIds.slice(i, i + batchSize);
const { data: details } = await client.core.getObjects({
objectIds: batch,
include: { content: true, display: true },
});
allDetails.push(...details);
}
return allDetails;
}
控制请求频率
避免过于频繁的 API 请求:
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchWithRateLimit<T>(
fetcher: (cursor?: string | null) => Promise<PaginatedResult<T>>,
delayMs: number = 100,
): Promise<T[]> {
const allItems: T[] = [];
let cursor: string | null | undefined = undefined;
let hasNextPage = true;
while (hasNextPage) {
const page = await fetcher(cursor);
allItems.push(...page.data);
hasNextPage = page.hasNextPage;
cursor = page.nextCursor;
if (hasNextPage) await delay(delayMs);
}
return allItems;
}
小结
- Sui API 使用基于 cursor 的分页机制,通过
nextCursor和hasNextPage控制 - 使用 while 循环遍历所有页面获取完整数据集
- 可以创建通用的
fetchAllPages工具函数简化分页代码 - 获取详情时优先使用
client.core.getObjects批量查询 - 注意控制请求频率和设置最大结果数,避免过载