Skip to content

Commit b5dd826

Browse files
d4mrarcoraven
andauthored
feat: Add getTxLogs route to retrieve transaction logs (#656)
* feat: Add getTxLogs route to retrieve transaction logs * CR comments, accept parseLogs bool * fix: Ensure either queue ID or transaction hash is provided in getTransactionLogs --------- Co-authored-by: Phillip Ho <arcoraven@gmail.com>
1 parent 75fe148 commit b5dd826

File tree

3 files changed

+239
-1
lines changed

3 files changed

+239
-1
lines changed

src/server/routes/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FastifyInstance } from "fastify";
1+
import type { FastifyInstance } from "fastify";
22
import { getNonceDetailsRoute } from "./admin/nonces";
33
import { getTransactionDetails } from "./admin/transaction";
44
import { createAccessToken } from "./auth/access-tokens/create";
@@ -93,6 +93,7 @@ import { revokeRelayer } from "./relayer/revoke";
9393
import { updateRelayer } from "./relayer/update";
9494
import { healthCheck } from "./system/health";
9595
import { queueStatus } from "./system/queue";
96+
import { getTransactionLogs } from "./transaction/blockchain/getLogs";
9697
import { getTxHashReceipt } from "./transaction/blockchain/getTxReceipt";
9798
import { getUserOpReceipt } from "./transaction/blockchain/getUserOpReceipt";
9899
import { sendSignedTransaction } from "./transaction/blockchain/sendSignedTx";
@@ -226,6 +227,7 @@ export const withRoutes = async (fastify: FastifyInstance) => {
226227
await fastify.register(sendSignedUserOp);
227228
await fastify.register(getTxHashReceipt);
228229
await fastify.register(getUserOpReceipt);
230+
await fastify.register(getTransactionLogs);
229231

230232
// Extensions
231233
await fastify.register(accountFactoryRoutes);
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { Type, type Static } from "@sinclair/typebox";
2+
import type { AbiEvent } from "abitype";
3+
import type { FastifyInstance } from "fastify";
4+
import { StatusCodes } from "http-status-codes";
5+
import superjson from "superjson";
6+
import {
7+
eth_getTransactionReceipt,
8+
getContract,
9+
getRpcClient,
10+
parseEventLogs,
11+
prepareEvent,
12+
type Hex,
13+
} from "thirdweb";
14+
import { resolveContractAbi } from "thirdweb/contract";
15+
import { TransactionDB } from "../../../../db/transactions/db";
16+
import { getChain } from "../../../../utils/chain";
17+
import { thirdwebClient } from "../../../../utils/sdk";
18+
import { createCustomError } from "../../../middleware/error";
19+
import { AddressSchema, TransactionHashSchema } from "../../../schemas/address";
20+
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
21+
import { getChainIdFromChain } from "../../../utils/chain";
22+
23+
// INPUT
24+
const requestQuerystringSchema = Type.Object({
25+
chain: Type.String({
26+
examples: ["80002"],
27+
description: "Chain ID or name",
28+
}),
29+
queueId: Type.Optional(
30+
Type.String({
31+
description: "The queue ID for a mined transaction.",
32+
}),
33+
),
34+
transactionHash: Type.Optional({
35+
...TransactionHashSchema,
36+
description: "The transaction hash for a mined transaction.",
37+
}),
38+
parseLogs: Type.Optional(
39+
Type.Boolean({
40+
description:
41+
"If true, parse the raw logs as events defined in the contract ABI. (Default: true)",
42+
}),
43+
),
44+
});
45+
46+
// OUTPUT
47+
const LogSchema = Type.Object({
48+
address: AddressSchema,
49+
topics: Type.Array(Type.String()),
50+
data: Type.String(),
51+
blockNumber: Type.String(),
52+
transactionHash: TransactionHashSchema,
53+
transactionIndex: Type.Number(),
54+
blockHash: Type.String(),
55+
logIndex: Type.Number(),
56+
removed: Type.Boolean(),
57+
});
58+
59+
const ParsedLogSchema = Type.Object({
60+
...LogSchema.properties,
61+
eventName: Type.String(),
62+
args: Type.Unknown({
63+
description: "Event arguments.",
64+
examples: [
65+
{
66+
from: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
67+
to: "0xdeadbeeefdeadbeeefdeadbeeefdeadbeeefdead",
68+
value: "1000000000000000000n",
69+
},
70+
],
71+
}),
72+
});
73+
74+
export const responseBodySchema = Type.Object({
75+
result: Type.Union([
76+
// ParsedLogSchema is listed before LogSchema because it is more specific.
77+
Type.Array(ParsedLogSchema),
78+
Type.Array(LogSchema),
79+
]),
80+
});
81+
82+
responseBodySchema.example = {
83+
result: [
84+
{
85+
eventName: "Transfer",
86+
args: {
87+
from: "0x0000000000000000000000000000000000000000",
88+
to: "0x71B6267b5b2b0B64EE058C3D27D58e4E14e7327f",
89+
value: "1000000000000000000n",
90+
},
91+
address: "0x71b6267b5b2b0b64ee058c3d27d58e4e14e7327f",
92+
topics: [
93+
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
94+
"0x0000000000000000000000000000000000000000000000000000000000000000",
95+
"0x00000000000000000000000071b6267b5b2b0b64ee058c3d27d58e4e14e7327f",
96+
],
97+
data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
98+
blockNumber: "79326434",
99+
transactionHash:
100+
"0x568eb49d738f7c02ebb24aa329efcf10883d951b1e13aa000b0e073d54a0246e",
101+
transactionIndex: 1,
102+
blockHash:
103+
"0xaffbcf3232a76152206de5f6999c549404efc76060a34f8826b90c95993464c3",
104+
logIndex: 0,
105+
removed: false,
106+
},
107+
],
108+
};
109+
110+
export async function getTransactionLogs(fastify: FastifyInstance) {
111+
fastify.route<{
112+
Querystring: Static<typeof requestQuerystringSchema>;
113+
Reply: Static<typeof responseBodySchema>;
114+
}>({
115+
method: "GET",
116+
url: "/transaction/logs",
117+
schema: {
118+
summary: "Get transaction logs",
119+
description:
120+
"Get transaction logs for a mined transaction. A tranasction queue ID or hash must be provided. Set `parseLogs` to parse the event logs.",
121+
tags: ["Transaction"],
122+
operationId: "getTransactionLogs",
123+
querystring: requestQuerystringSchema,
124+
response: {
125+
...standardResponseSchema,
126+
[StatusCodes.OK]: responseBodySchema,
127+
},
128+
},
129+
handler: async (request, reply) => {
130+
const {
131+
chain: inputChain,
132+
queueId,
133+
transactionHash,
134+
parseLogs = true,
135+
} = request.query;
136+
137+
const chainId = await getChainIdFromChain(inputChain);
138+
const chain = await getChain(chainId);
139+
const rpcRequest = getRpcClient({
140+
client: thirdwebClient,
141+
chain,
142+
});
143+
144+
if (!queueId && !transactionHash) {
145+
throw createCustomError(
146+
"Either a queue ID or transaction hash must be provided.",
147+
StatusCodes.BAD_REQUEST,
148+
"MISSING_TRANSACTION_ID",
149+
);
150+
}
151+
152+
// Get the transaction hash from the provided input.
153+
let hash: Hex | undefined;
154+
if (queueId) {
155+
const transaction = await TransactionDB.get(queueId);
156+
if (transaction?.status === "mined") {
157+
hash = transaction.transactionHash;
158+
}
159+
} else if (transactionHash) {
160+
hash = transactionHash as Hex;
161+
}
162+
if (!hash) {
163+
throw createCustomError(
164+
"Could not find transaction, or transaction is not mined.",
165+
StatusCodes.BAD_REQUEST,
166+
"TRANSACTION_NOT_MINED",
167+
);
168+
}
169+
170+
// Try to get the receipt.
171+
const transactionReceipt = await eth_getTransactionReceipt(rpcRequest, {
172+
hash,
173+
});
174+
if (!transactionReceipt) {
175+
throw createCustomError(
176+
"Cannot get logs for a transaction that is not mined.",
177+
StatusCodes.BAD_REQUEST,
178+
"TRANSACTION_NOT_MINED",
179+
);
180+
}
181+
182+
if (!parseLogs) {
183+
return reply.status(StatusCodes.OK).send({
184+
result: superjson.serialize(transactionReceipt.logs).json as Static<
185+
typeof LogSchema
186+
>[],
187+
});
188+
}
189+
190+
if (!transactionReceipt.to) {
191+
throw createCustomError(
192+
"Transaction logs are only supported for contract calls.",
193+
StatusCodes.BAD_REQUEST,
194+
"TRANSACTION_LOGS_UNAVAILABLE",
195+
);
196+
}
197+
198+
const contract = getContract({
199+
address: transactionReceipt.to,
200+
chain,
201+
client: thirdwebClient,
202+
});
203+
204+
const abi: AbiEvent[] = await resolveContractAbi(contract);
205+
const eventSignatures = abi.filter((item) => item.type === "event");
206+
if (eventSignatures.length === 0) {
207+
throw createCustomError(
208+
"No events found in contract or could not resolve contract ABI",
209+
StatusCodes.BAD_REQUEST,
210+
"NO_EVENTS_FOUND",
211+
);
212+
}
213+
214+
const preparedEvents = eventSignatures.map((signature) =>
215+
prepareEvent({ signature }),
216+
);
217+
const parsedLogs = parseEventLogs({
218+
events: preparedEvents,
219+
logs: transactionReceipt.logs,
220+
});
221+
222+
reply.status(StatusCodes.OK).send({
223+
result: superjson.serialize(parsedLogs).json as Static<
224+
typeof ParsedLogSchema
225+
>[],
226+
});
227+
},
228+
});
229+
}

src/server/schemas/address.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ export const AddressSchema = Type.RegExp(/^0x[a-fA-F0-9]{40}$/, {
1414
description: "A contract or wallet address",
1515
examples: ["0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4"],
1616
});
17+
18+
export const TransactionHashSchema = Type.RegExp(/^0x[a-fA-F0-9]{64}$/, {
19+
description: "A transaction hash",
20+
examples: [
21+
"0x1f31b57601a6f90312fd5e57a2924bc8333477de579ee37b197a0681ab438431",
22+
],
23+
});

0 commit comments

Comments
 (0)