-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor: Add readMulticall route for batch contract reads #773
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import { Type, type Static } from "@sinclair/typebox"; | ||
import type { FastifyInstance } from "fastify"; | ||
import { StatusCodes } from "http-status-codes"; | ||
import SuperJSON from "superjson"; | ||
import { | ||
getContract, | ||
prepareContractCall, | ||
readContract, | ||
resolveMethod, | ||
} from "thirdweb"; | ||
import { prepareMethod } from "thirdweb/contract"; | ||
import { resolvePromisedValue, type AbiFunction } from "thirdweb/utils"; | ||
import { decodeAbiParameters } from "viem/utils"; | ||
import { getChain } from "../../../../utils/chain"; | ||
import { prettifyError } from "../../../../utils/error"; | ||
import { thirdwebClient } from "../../../../utils/sdk"; | ||
import { createCustomError } from "../../../middleware/error"; | ||
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas"; | ||
import { getChainIdFromChain } from "../../../utils/chain"; | ||
import { bigNumberReplacer } from "../../../utils/convertor"; | ||
|
||
const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"; | ||
|
||
const MULTICALL3_AGGREGATE_ABI = | ||
"function aggregate3((address target, bool allowFailure, bytes callData)[] calls) external payable returns ((bool success, bytes returnData)[])"; | ||
|
||
const readCallRequestItemSchema = Type.Object({ | ||
contractAddress: Type.String(), | ||
functionName: Type.String(), | ||
functionAbi: Type.Optional(Type.String()), | ||
args: Type.Optional(Type.Array(Type.Any())), | ||
}); | ||
|
||
const readMulticallRequestSchema = Type.Object({ | ||
calls: Type.Array(readCallRequestItemSchema), | ||
multicallAddress: Type.Optional(Type.String()), | ||
}); | ||
|
||
const responseSchema = Type.Object({ | ||
results: Type.Array( | ||
Type.Object({ | ||
success: Type.Boolean(), | ||
result: Type.Any(), | ||
}), | ||
), | ||
}); | ||
|
||
const paramsSchema = Type.Object({ | ||
chain: Type.String(), | ||
}); | ||
|
||
type RouteGeneric = { | ||
Params: { chain: string }; | ||
Body: Static<typeof readMulticallRequestSchema>; | ||
Reply: Static<typeof responseSchema>; | ||
}; | ||
|
||
export async function readMulticall(fastify: FastifyInstance) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small nit and non-blocking: I started calling route names So having that suffix is nice for clarity. |
||
fastify.route<RouteGeneric>({ | ||
method: "POST", | ||
url: "/contract/:chain/read-batch", | ||
schema: { | ||
summary: "Batch read from multiple contracts", | ||
description: | ||
"Execute multiple contract read operations in a single call using Multicall3", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO don't need to mention multicall3 as it's a low level implementation detail. You can refer to "using multicall" since that's a web3 concept. |
||
tags: ["Contract"], | ||
operationId: "readMulticall", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
params: paramsSchema, | ||
body: readMulticallRequestSchema, | ||
response: { | ||
...standardResponseSchema, | ||
[StatusCodes.OK]: responseSchema, | ||
}, | ||
}, | ||
handler: async (request, reply) => { | ||
const { chain: chainSlug } = request.params; | ||
const { calls, multicallAddress = MULTICALL3_ADDRESS } = request.body; | ||
|
||
const chainId = await getChainIdFromChain(chainSlug); | ||
const chain = await getChain(chainId); | ||
|
||
try { | ||
// Encode each read call | ||
const encodedCalls = await Promise.all( | ||
calls.map(async (call) => { | ||
const contract = await getContract({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't have to await getContract btw |
||
client: thirdwebClient, | ||
chain, | ||
address: call.contractAddress, | ||
}); | ||
|
||
const method = | ||
(call.functionAbi as unknown as AbiFunction) ?? | ||
(await resolveMethod(call.functionName)(contract)); | ||
|
||
const transaction = prepareContractCall({ | ||
contract, | ||
method, | ||
params: call.args || [], | ||
// stubbing gas values so that the call can be encoded | ||
maxFeePerGas: 30n, | ||
maxPriorityFeePerGas: 1n, | ||
value: 0n, | ||
Comment on lines
+100
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these need to be stubbed? What happens if we leave them undefined? |
||
}); | ||
|
||
const calldata = await resolvePromisedValue(transaction.data); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use resolvePromisedValue. I should add a import { encode } from "thirdweb";
const callData = await encode(transaction); |
||
if (!calldata) { | ||
throw new Error("Failed to encode call data"); | ||
} | ||
|
||
return { | ||
target: call.contractAddress, | ||
abiFunction: method, | ||
allowFailure: true, | ||
callData: calldata, | ||
}; | ||
}), | ||
); | ||
|
||
// Get Multicall3 contract | ||
const multicall = await getContract({ | ||
chain, | ||
address: multicallAddress, | ||
client: thirdwebClient, | ||
}); | ||
|
||
// Execute batch read | ||
const results = await readContract({ | ||
contract: multicall, | ||
method: MULTICALL3_AGGREGATE_ABI, | ||
params: [encodedCalls], | ||
}); | ||
|
||
// Process results | ||
const processedResults = results.map((result: unknown, i) => { | ||
const { success, returnData } = result as { | ||
success: boolean; | ||
returnData: unknown; | ||
}; | ||
|
||
const [_sig, _inputs, outputs] = prepareMethod( | ||
encodedCalls[i].abiFunction, | ||
); | ||
|
||
const decoded = decodeAbiParameters( | ||
outputs, | ||
returnData as `0x${string}`, | ||
); | ||
|
||
return { | ||
success, | ||
result: success ? bigNumberReplacer(decoded) : null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not need bigNumberReplacer. We're on v5 SDK which does not rely on ethers. |
||
}; | ||
}); | ||
|
||
reply.status(StatusCodes.OK).send({ | ||
results: SuperJSON.serialize(processedResults).json as Static< | ||
typeof responseSchema | ||
>["results"], | ||
}); | ||
} catch (e) { | ||
throw createCustomError( | ||
prettifyError(e), | ||
StatusCodes.BAD_REQUEST, | ||
"BAD_REQUEST", | ||
); | ||
} | ||
}, | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a description so people know they don't need to set this: