diff --git a/.changeset/rpc-batch-split-retry.md b/.changeset/rpc-batch-split-retry.md new file mode 100644 index 0000000..ab046b5 --- /dev/null +++ b/.changeset/rpc-batch-split-retry.md @@ -0,0 +1,5 @@ +--- +"@polymarket/client": patch +--- + +Retry rejected JSON-RPC `eth_call` batches by recursively splitting them into smaller batches. diff --git a/packages/client/src/rpc.test.ts b/packages/client/src/rpc.test.ts index c5e1695..5c8706b 100644 --- a/packages/client/src/rpc.test.ts +++ b/packages/client/src/rpc.test.ts @@ -79,6 +79,50 @@ describe('JsonRpcClient', () => { ).resolves.toEqual(['0xaaaa', '0xbbbb']); }); + it('recovers failed eth_call batches while preserving result order', async () => { + const to = expectEvmAddress('0x0000000000000000000000000000000000000001'); + let requestCount = 0; + + server.use( + http.post(root, async ({ request }) => { + requestCount += 1; + + const body = (await request.json()) as + | { + id: number; + params: [{ data: string }]; + } + | unknown[]; + + if (Array.isArray(body)) { + return new HttpResponse(null, { status: 500 }); + } + + return HttpResponse.json({ + jsonrpc: '2.0', + id: body.id, + result: body.params[0].data, + }); + }), + ); + const client = new JsonRpcClient({ url: root }); + + await expect( + client.ethCallBatch([ + { to, data: '0x11111111' }, + { to, data: '0x22222222' }, + { to, data: '0x33333333' }, + { to, data: '0x44444444' }, + ]), + ).resolves.toEqual([ + '0x11111111', + '0x22222222', + '0x33333333', + '0x44444444', + ]); + expect(requestCount).toBe(7); + }); + it('wraps JSON-RPC errors as rejected requests', async () => { const to = expectEvmAddress('0x0000000000000000000000000000000000000001'); server.use( diff --git a/packages/client/src/rpc.ts b/packages/client/src/rpc.ts index 776278e..4e377c2 100644 --- a/packages/client/src/rpc.ts +++ b/packages/client/src/rpc.ts @@ -80,6 +80,36 @@ export class JsonRpcClient { return []; } + return this.#ethCallBatchWithSplit(requests); + } + + async #ethCallBatchWithSplit( + requests: readonly EthCallRequest[], + ): Promise { + if (requests.length === 1) { + return [await this.ethCall(requests[0] as EthCallRequest)]; + } + + try { + return await this.#postEthCallBatch(requests); + } catch (error) { + if (!(error instanceof RequestRejectedError) || error.status < 500) { + throw error; + } + + const midpoint = Math.ceil(requests.length / 2); + const [left, right] = await Promise.all([ + this.#ethCallBatchWithSplit(requests.slice(0, midpoint)), + this.#ethCallBatchWithSplit(requests.slice(midpoint)), + ]); + + return [...left, ...right]; + } + } + + async #postEthCallBatch( + requests: readonly EthCallRequest[], + ): Promise { const responses = await this.#postBatch( requests.map((request, index) => ({ jsonrpc: '2.0',