Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"prom-client": "^15.1.3",
"prool": "^0.0.16",
"superjson": "^2.2.1",
"thirdweb": "5.61.3",
"thirdweb": "5.69.0",
"uuid": "^9.0.1",
"winston": "^3.14.1",
"zod": "^3.23.8"
Expand Down
170 changes: 170 additions & 0 deletions src/server/routes/contract/read/read-batch.ts
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()),
Copy link
Contributor

@arcoraven arcoraven Nov 23, 2024

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:

Override a specific multicall contract. Defaults to Multicall3.

});

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit and non-blocking: I started calling route names readMulticallRoute. The reason is we have so many exported functions that end up having the same name (for example: createAccessToken might be the route, or the cached call, or the raw db call, who knows?)

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",
Copy link
Contributor

@arcoraven arcoraven Nov 23, 2024

Choose a reason for hiding this comment

The 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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readBatch is preferred since "multicall" is the underlying approach but the use case is a batch read.

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({

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

@arcoraven arcoraven Nov 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use resolvePromisedValue. I should add a @deprecated tag on the function. Use this instead:

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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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",
);
}
},
});
}
2 changes: 2 additions & 0 deletions src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { extractEvents } from "./contract/metadata/events";
import { getContractExtensions } from "./contract/metadata/extensions";
import { extractFunctions } from "./contract/metadata/functions";
import { readContract } from "./contract/read/read";
import { readMulticall } from "./contract/read/read-batch";
import { getRoles } from "./contract/roles/read/get";
import { getAllRoles } from "./contract/roles/read/getAll";
import { grantRole } from "./contract/roles/write/grant";
Expand Down Expand Up @@ -190,6 +191,7 @@ export const withRoutes = async (fastify: FastifyInstance) => {

// Generic
await fastify.register(readContract);
await fastify.register(readMulticall);
await fastify.register(writeToContract);

// Contract Events
Expand Down
Loading
Loading