Skip to content

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

Merged
merged 8 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
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 {
encode,
getContract,
prepareContractCall,
readContract,
resolveMethod,
} from "thirdweb";
import { prepareMethod } from "thirdweb/contract";
import { decodeAbiParameters } from "viem/utils";
import type { AbiFunction } from "viem";
import { createCustomError } from "../../../middleware/error";
import { getChainIdFromChain } from "../../../utils/chain";
import { standardResponseSchema } from "../../../schemas/shared-api-schemas";
import { getChain } from "../../../../shared/utils/chain";
import { thirdwebClient } from "../../../../shared/utils/sdk";
import { prettifyError } from "../../../../shared/utils/error";

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({
description: `Address of the multicall contract to use. If omitted, multicall3 contract will be used (${MULTICALL3_ADDRESS}).`,
}),
),
});

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 readMulticallRoute(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.

Nit: I consider multicall the implementation, and any interface higher than this method shouldn't need to know about it. So var/fn names (even if not user facing) don't need to have multicall in them. Maybe batchReadRoute or readBatchRoute?

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 Multicall",
tags: ["Contract"],
operationId: "readBatch",
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 = getContract({
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 || [],
});

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],
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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 ? decoded : null,
};
});

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 @@ -68,6 +68,7 @@
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";

Check failure on line 71 in src/server/routes/index.ts

View workflow job for this annotation

GitHub Actions / build

Module '"./contract/read/read-batch"' has no exported member 'readMulticall'.
import { getRoles } from "./contract/roles/read/get";
import { getAllRoles } from "./contract/roles/read/get-all";
import { grantRole } from "./contract/roles/write/grant";
Expand Down Expand Up @@ -192,6 +193,7 @@

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

// Contract Events
Expand Down
16 changes: 14 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2372,7 +2372,7 @@
dependencies:
"@noble/hashes" "1.4.0"

"@noble/curves@1.4.2", "@noble/curves@~1.4.0":
"@noble/curves@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9"
integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==
Expand All @@ -2386,6 +2386,13 @@
dependencies:
"@noble/hashes" "1.6.0"

"@noble/curves@~1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b"
integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==
dependencies:
"@noble/hashes" "1.5.0"

"@noble/hashes@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
Expand All @@ -2396,6 +2403,11 @@
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==

"@noble/hashes@1.5.0", "@noble/hashes@~1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0"
integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==

"@noble/hashes@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5"
Expand Down Expand Up @@ -3262,7 +3274,7 @@
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865"
integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ==

"@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6":
"@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6", "@scure/base@~1.1.7":
version "1.1.9"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1"
integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==
Expand Down
Loading