-
Notifications
You must be signed in to change notification settings - Fork 92
feat: batching for reads and smart-account writes #773
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
Changes from 2 commits
adb4d58
302f1f9
b818ef2
e56086c
82b8ce6
f24e84b
1904ce5
215c458
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) { | ||
d4mr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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", | ||
d4mr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
tags: ["Contract"], | ||
operationId: "readMulticall", | ||
d4mr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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({ | ||
d4mr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, | ||
d4mr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
const calldata = await resolvePromisedValue(transaction.data); | ||
d4mr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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], | ||
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. i think there was a limit to how much data you can pack in here, did you try a very large read? 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. There's definitely a limit, but failing and forwarding the error should be fine, no? 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. Yeah should be fine |
||
}); | ||
|
||
// 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, | ||
d4mr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
}); | ||
|
||
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", | ||
); | ||
} | ||
}, | ||
}); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.