Skip to content

Commit adb4d58

Browse files
committed
Refactor: Add readMulticall route for batch contract reads
1 parent 4e9ea68 commit adb4d58

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { Type, type Static } from "@sinclair/typebox";
2+
import type { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import {
5+
getContract,
6+
prepareContractCall,
7+
readContract,
8+
resolveMethod,
9+
} from "thirdweb";
10+
import { prepareMethod } from "thirdweb/dist/types/utils/abi/prepare-method";
11+
import { resolvePromisedValue, type AbiFunction } from "thirdweb/utils";
12+
import { decodeAbiParameters } from "viem/utils";
13+
import { getChain } from "../../../../utils/chain";
14+
import { prettifyError } from "../../../../utils/error";
15+
import { thirdwebClient } from "../../../../utils/sdk";
16+
import { createCustomError } from "../../../middleware/error";
17+
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
18+
import { getChainIdFromChain } from "../../../utils/chain";
19+
import { bigNumberReplacer } from "../../../utils/convertor";
20+
21+
const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
22+
23+
const MULTICALL3_AGGREGATE_ABI =
24+
"function aggregate3((address target, bool allowFailure, bytes callData)[] calls) external payable returns ((bool success, bytes returnData)[])";
25+
26+
const readCallRequestItemSchema = Type.Object({
27+
contractAddress: Type.String(),
28+
functionName: Type.String(),
29+
functionAbi: Type.Optional(Type.String()),
30+
args: Type.Optional(Type.Array(Type.Any())),
31+
});
32+
33+
const readMulticallRequestSchema = Type.Object({
34+
calls: Type.Array(readCallRequestItemSchema),
35+
multicallAddress: Type.Optional(Type.String()),
36+
});
37+
38+
const responseSchema = Type.Object({
39+
results: Type.Array(
40+
Type.Object({
41+
success: Type.Boolean(),
42+
result: Type.Any(),
43+
}),
44+
),
45+
});
46+
47+
const paramsSchema = Type.Object({
48+
chain: Type.String(),
49+
});
50+
51+
type RouteGeneric = {
52+
Params: { chain: string };
53+
Body: Static<typeof readMulticallRequestSchema>;
54+
Reply: Static<typeof responseSchema>;
55+
};
56+
57+
export async function readMulticall(fastify: FastifyInstance) {
58+
fastify.route<RouteGeneric>({
59+
method: "POST",
60+
url: "/contract/:chain/read-batch",
61+
schema: {
62+
summary: "Batch read from multiple contracts",
63+
description:
64+
"Execute multiple contract read operations in a single call using Multicall3",
65+
tags: ["Contract"],
66+
operationId: "readMulticall",
67+
params: paramsSchema,
68+
body: readMulticallRequestSchema,
69+
response: {
70+
...standardResponseSchema,
71+
[StatusCodes.OK]: responseSchema,
72+
},
73+
},
74+
handler: async (request, reply) => {
75+
const { chain: chainSlug } = request.params;
76+
const { calls, multicallAddress = MULTICALL3_ADDRESS } = request.body;
77+
78+
const chainId = await getChainIdFromChain(chainSlug);
79+
const chain = await getChain(chainId);
80+
81+
try {
82+
// Encode each read call
83+
const encodedCalls = await Promise.all(
84+
calls.map(async (call) => {
85+
const contract = await getContract({
86+
client: thirdwebClient,
87+
chain,
88+
address: call.contractAddress,
89+
});
90+
91+
const method =
92+
(call.functionAbi as unknown as AbiFunction) ??
93+
(await resolveMethod(call.functionName)(contract));
94+
95+
const transaction = prepareContractCall({
96+
contract,
97+
method,
98+
params: call.args || [],
99+
// stubbing gas values so that the call can be encoded
100+
maxFeePerGas: 30n,
101+
maxPriorityFeePerGas: 1n,
102+
value: 0n,
103+
});
104+
105+
const calldata = await resolvePromisedValue(transaction.data);
106+
if (!calldata) {
107+
throw new Error("Failed to encode call data");
108+
}
109+
110+
return {
111+
target: call.contractAddress,
112+
abiFunction: method,
113+
allowFailure: true,
114+
callData: calldata,
115+
};
116+
}),
117+
);
118+
119+
// Get Multicall3 contract
120+
const multicall = await getContract({
121+
chain,
122+
address: multicallAddress,
123+
client: thirdwebClient,
124+
});
125+
126+
// Execute batch read
127+
const results = await readContract({
128+
contract: multicall,
129+
method: MULTICALL3_AGGREGATE_ABI,
130+
params: [encodedCalls],
131+
});
132+
133+
// Process results
134+
const processedResults = results.map((result: unknown, i) => {
135+
const { success, returnData } = result as {
136+
success: boolean;
137+
returnData: unknown;
138+
};
139+
140+
const [_sig, _inputs, outputs] = prepareMethod(
141+
encodedCalls[i].abiFunction,
142+
);
143+
144+
const decoded = decodeAbiParameters(
145+
outputs,
146+
returnData as `0x${string}`,
147+
);
148+
149+
return {
150+
success,
151+
result: success ? bigNumberReplacer(decoded) : null,
152+
};
153+
});
154+
155+
reply.status(StatusCodes.OK).send({
156+
results: processedResults,
157+
});
158+
} catch (e) {
159+
throw createCustomError(
160+
prettifyError(e),
161+
StatusCodes.BAD_REQUEST,
162+
"BAD_REQUEST",
163+
);
164+
}
165+
},
166+
});
167+
}

src/server/routes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import { extractEvents } from "./contract/metadata/events";
6767
import { getContractExtensions } from "./contract/metadata/extensions";
6868
import { extractFunctions } from "./contract/metadata/functions";
6969
import { readContract } from "./contract/read/read";
70+
import { readMulticall } from "./contract/read/read-batch";
7071
import { getRoles } from "./contract/roles/read/get";
7172
import { getAllRoles } from "./contract/roles/read/getAll";
7273
import { grantRole } from "./contract/roles/write/grant";
@@ -190,6 +191,7 @@ export const withRoutes = async (fastify: FastifyInstance) => {
190191

191192
// Generic
192193
await fastify.register(readContract);
194+
await fastify.register(readMulticall);
193195
await fastify.register(writeToContract);
194196

195197
// Contract Events

0 commit comments

Comments
 (0)