Skip to content

Commit 8e38da9

Browse files
authored
Merge pull request #14494 from LedgerHQ/fix/sui-batch-multi-get-objects-limit
fix(coin-sui): batch multiGetObjects calls to respect 50-object RPC limit
2 parents 8c6c877 + 8f15ee0 commit 8e38da9

File tree

3 files changed

+136
-3
lines changed

3 files changed

+136
-3
lines changed

.changeset/sharp-impalas-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ledgerhq/coin-sui": patch
3+
---
4+
5+
Batch multiGetObjects calls in chunks of 50 to fix "Input exceeds limit of 50" error on fragmented accounts

libs/coin-modules/coin-sui/src/network/sdk.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
SuiTransactionBlockResponse,
66
SuiTransactionBlockKind,
77
PaginatedTransactionResponse,
8+
SuiObjectResponse,
89
} from "@mysten/sui/client";
910
import { BigNumber } from "bignumber.js";
1011
import coinConfig from "../config";
@@ -2273,3 +2274,96 @@ describe("getCoinsForAmount", () => {
22732274
});
22742275
});
22752276
});
2277+
2278+
describe("withBatchedMultiGetObjects", () => {
2279+
const createMockClient = () => {
2280+
const multiGetObjects = jest.fn(
2281+
async (params: { ids: string[]; options?: Record<string, boolean> }) => {
2282+
if (params.ids.length > 50) {
2283+
throw new Error("Input exceeds limit of 50");
2284+
}
2285+
return params.ids.map(
2286+
id =>
2287+
({
2288+
data: {
2289+
objectId: id,
2290+
version: "1",
2291+
digest: `digest-${id}`,
2292+
},
2293+
}) as SuiObjectResponse,
2294+
);
2295+
},
2296+
);
2297+
return { client: { multiGetObjects } as unknown as SuiClient, multiGetObjects };
2298+
};
2299+
2300+
it("should pass through when <= 50 objects", async () => {
2301+
// GIVEN
2302+
const { client, multiGetObjects } = createMockClient();
2303+
const ids = Array.from({ length: 30 }, (_, i) => `0xobj${i}`);
2304+
2305+
// WHEN
2306+
const batched = sdk.withBatchedMultiGetObjects(client);
2307+
const result = await batched.multiGetObjects({ ids, options: { showBcs: true } });
2308+
2309+
// THEN
2310+
expect(multiGetObjects).toHaveBeenCalledTimes(1);
2311+
expect(result).toHaveLength(30);
2312+
});
2313+
2314+
it("should batch when > 50 objects", async () => {
2315+
// GIVEN
2316+
const { client, multiGetObjects } = createMockClient();
2317+
const ids = Array.from({ length: 120 }, (_, i) => `0xobj${i}`);
2318+
2319+
// WHEN
2320+
const batched = sdk.withBatchedMultiGetObjects(client);
2321+
const result = await batched.multiGetObjects({ ids, options: { showBcs: true } });
2322+
2323+
// THEN
2324+
expect(multiGetObjects).toHaveBeenCalledTimes(3);
2325+
expect(multiGetObjects).toHaveBeenNthCalledWith(1, {
2326+
ids: ids.slice(0, 50),
2327+
options: { showBcs: true },
2328+
});
2329+
expect(multiGetObjects).toHaveBeenNthCalledWith(2, {
2330+
ids: ids.slice(50, 100),
2331+
options: { showBcs: true },
2332+
});
2333+
expect(multiGetObjects).toHaveBeenNthCalledWith(3, {
2334+
ids: ids.slice(100, 120),
2335+
options: { showBcs: true },
2336+
});
2337+
expect(result).toHaveLength(120);
2338+
expect(result[0].data?.objectId).toBe("0xobj0");
2339+
expect(result[119].data?.objectId).toBe("0xobj119");
2340+
});
2341+
2342+
it("should handle exactly 50 objects without batching", async () => {
2343+
// GIVEN
2344+
const { client, multiGetObjects } = createMockClient();
2345+
const ids = Array.from({ length: 50 }, (_, i) => `0xobj${i}`);
2346+
2347+
// WHEN
2348+
const batched = sdk.withBatchedMultiGetObjects(client);
2349+
const result = await batched.multiGetObjects({ ids });
2350+
2351+
// THEN
2352+
expect(multiGetObjects).toHaveBeenCalledTimes(1);
2353+
expect(result).toHaveLength(50);
2354+
});
2355+
2356+
it("should handle exactly 51 objects with batching", async () => {
2357+
// GIVEN
2358+
const { client, multiGetObjects } = createMockClient();
2359+
const ids = Array.from({ length: 51 }, (_, i) => `0xobj${i}`);
2360+
2361+
// WHEN
2362+
const batched = sdk.withBatchedMultiGetObjects(client);
2363+
const result = await batched.multiGetObjects({ ids });
2364+
2365+
// THEN
2366+
expect(multiGetObjects).toHaveBeenCalledTimes(2);
2367+
expect(result).toHaveLength(51);
2368+
});
2369+
});

libs/coin-modules/coin-sui/src/network/sdk.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type AsyncApiFunction<T> = (api: SuiClient) => Promise<T>;
5454

5555
export const TRANSACTIONS_LIMIT_PER_QUERY = 50;
5656
export const TRANSACTIONS_LIMIT = 300;
57+
const MULTI_GET_OBJECTS_LIMIT = 50;
5758
const BLOCK_HEIGHT = 5; // sui has no block height metainfo, we use it simulate proper icon statuses in apps
5859

5960
export const DEFAULT_COIN_TYPE = "0x2::sui::SUI";
@@ -98,6 +99,39 @@ export async function withApi<T>(execute: AsyncApiFunction<T>) {
9899
return result;
99100
}
100101

102+
/**
103+
* Wraps a SuiClient to batch multiGetObjects calls in chunks of 50,
104+
* working around the SUI RPC limit.
105+
*/
106+
export function withBatchedMultiGetObjects(client: SuiClient): SuiClient {
107+
return new Proxy(client, {
108+
get(target, prop, _receiver) {
109+
if (prop === "multiGetObjects") {
110+
return async (params: Parameters<SuiClient["multiGetObjects"]>[0]) => {
111+
const { ids } = params;
112+
if (ids.length <= MULTI_GET_OBJECTS_LIMIT) {
113+
return target.multiGetObjects(params);
114+
}
115+
const results = [];
116+
for (let i = 0; i < ids.length; i += MULTI_GET_OBJECTS_LIMIT) {
117+
const chunk = await target.multiGetObjects({
118+
...params,
119+
ids: ids.slice(i, i + MULTI_GET_OBJECTS_LIMIT),
120+
});
121+
results.push(...chunk);
122+
}
123+
return results;
124+
};
125+
}
126+
const value = Reflect.get(target, prop, target);
127+
if (typeof value === "function") {
128+
return value.bind(target);
129+
}
130+
return value;
131+
},
132+
});
133+
}
134+
101135
export const getAllBalancesCached = makeLRUCache(
102136
async (owner: string) =>
103137
withApi(
@@ -950,7 +984,7 @@ const createTransactionForDelegate = async (address: string, transaction: Create
950984
tx.setGasBudgetIfNotSet(ONE_SUI / 10);
951985

952986
const serialized = await tx.build({ client: api });
953-
const { bcsObjects } = await getInputObjects(tx, api);
987+
const { bcsObjects } = await getInputObjects(tx, withBatchedMultiGetObjects(api));
954988

955989
return { serialized, bcsObjects };
956990
});
@@ -981,7 +1015,7 @@ const createTransactionForUndelegate = async (address: string, transaction: Crea
9811015
tx.setGasBudgetIfNotSet(ONE_SUI / 10);
9821016

9831017
const serialized = await tx.build({ client: api });
984-
const { bcsObjects } = await getInputObjects(tx, api);
1018+
const { bcsObjects } = await getInputObjects(tx, withBatchedMultiGetObjects(api));
9851019

9861020
return { serialized, bcsObjects };
9871021
});
@@ -1015,7 +1049,7 @@ const createTransactionForOthers = async (address: string, transaction: CreateEx
10151049
}
10161050

10171051
const serialized = await tx.build({ client: api });
1018-
const { bcsObjects } = await getInputObjects(tx, api);
1052+
const { bcsObjects } = await getInputObjects(tx, withBatchedMultiGetObjects(api));
10191053

10201054
return { serialized, bcsObjects };
10211055
});

0 commit comments

Comments
 (0)