From c37b08d801b57a9d3584f1ed5f7a00359ca42481 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:45:56 +0200 Subject: [PATCH] refactor: better error handling when parsing events (#8) --- src/index.ts | 12 +++-- src/parse-transaction-receipts.ts | 88 ++++++++++++++++++------------- src/types.ts | 10 +++- src/utils/get-event-by-topic.ts | 14 +++++ 4 files changed, 82 insertions(+), 42 deletions(-) create mode 100644 src/utils/get-event-by-topic.ts diff --git a/src/index.ts b/src/index.ts index 0b198a9..e08370e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ +import assert from "assert/strict"; + // Types import type { HttpFunction, Request, Response, } from "@google-cloud/functions-framework"; +import { EventType } from "./types.js"; import parseTransactionReceipts from "./parse-transaction-receipts"; import sendDiscordNotification from "./send-discord-notification"; import sendTelegramNotification from "./send-telegram-notification"; @@ -17,16 +20,19 @@ export const watchdogNotifier: HttpFunction = async ( for (const parsedEvent of parsedEvents) { switch (parsedEvent.event.eventName) { - case "ProposalCreated": + case EventType.ProposalCreated: await sendDiscordNotification(parsedEvent.event, parsedEvent.txHash); await sendTelegramNotification(parsedEvent.event, parsedEvent.txHash); break; - case "MedianUpdated": + case EventType.MedianUpdated: // Acts a health check/heartbeat for the service, as it's a frequently emitted event console.info("[HealthCheck]: Block", parsedEvent.block); break; default: - throw new Error("Unknown event type", parsedEvent.event); + assert( + false, + `Unknown event type from payload: ${JSON.stringify(req.body)}`, + ); } } diff --git a/src/parse-transaction-receipts.ts b/src/parse-transaction-receipts.ts index 57125fa..42c7151 100644 --- a/src/parse-transaction-receipts.ts +++ b/src/parse-transaction-receipts.ts @@ -1,10 +1,13 @@ +import assert from "assert/strict"; + // External import { decodeEventLog } from "viem"; // Internal import GovernorABI from "./governor-abi.js"; -import { HealthCheckEvent, ProposalCreatedEvent } from "./types.js"; +import { EventType, HealthCheckEvent, ProposalCreatedEvent } from "./types.js"; import hasLogs from "./utils/has-logs.js"; +import getEventByTopic from "./utils/get-event-by-topic.js"; import isHealthCheckEvent from "./utils/is-health-check-event.js"; import isProposalCreatedEvent from "./utils/is-proposal-created-event.js"; import isTransactionReceipt from "./utils/is-transaction-receipt.js"; @@ -45,48 +48,59 @@ export default function parseTransactionReceipts( } for (const log of receipt.logs) { - if (!log.topics) { - throw new Error("No topics found in log"); - } + assert(log.topics && log.topics.length > 0, "No topics found in log"); - try { - const event = decodeEventLog({ - abi: GovernorABI, - data: log.data as `0x${string}`, - topics: log.topics as [ - signature: `0x${string}`, - ...args: `0x${string}`[], - ], - }); + const eventSignature = log.topics[0]; + const eventType = getEventByTopic(eventSignature); - if (isProposalCreatedEvent(event)) { - result.push({ - event, - txHash: log.transactionHash, + switch (eventType) { + case EventType.Unknown: + // It can happen that a single transaction fires multiple events, + // some of which we are not interested in + continue; + case EventType.ProposalCreated: { + const event = decodeEventLog({ + abi: GovernorABI, + data: log.data as `0x${string}`, + topics: log.topics as [ + signature: `0x${string}`, + ...args: `0x${string}`[], + ], }); - } - // eslint-disable-next-line no-empty - } catch {} - - try { - const event = decodeEventLog({ - abi: SortedOraclesABI, - data: log.data as `0x${string}`, - topics: log.topics as [ - signature: `0x${string}`, - ...args: `0x${string}`[], - ], - }); - if (isHealthCheckEvent(event)) { - result.push({ - block: Number(receipt.blockNumber), - event, - txHash: log.transactionHash, + if (isProposalCreatedEvent(event)) { + result.push({ + event, + txHash: log.transactionHash, + }); + } + break; + } + case EventType.MedianUpdated: { + const event = decodeEventLog({ + abi: SortedOraclesABI, + data: log.data as `0x${string}`, + topics: log.topics as [ + signature: `0x${string}`, + ...args: `0x${string}`[], + ], }); + + if (isHealthCheckEvent(event)) { + result.push({ + block: Number(receipt.blockNumber), + event, + txHash: log.transactionHash, + }); + } + break; } - // eslint-disable-next-line no-empty - } catch {} + default: + assert( + false, + `Unknown event type. Did you forget to add a new event?`, + ); + } } } diff --git a/src/types.ts b/src/types.ts index 24ca96f..47390d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,8 +26,14 @@ export interface LogsEntity { transactionIndex: string; } +export enum EventType { + Unknown = "Unknown", + ProposalCreated = "ProposalCreated", + MedianUpdated = "MedianUpdated", +} + export interface ProposalCreatedEvent { - eventName: "ProposalCreated"; + eventName: EventType.ProposalCreated; args: { calldatas: readonly `0x${string}`[]; description: string; @@ -43,7 +49,7 @@ export interface ProposalCreatedEvent { } export interface HealthCheckEvent { - eventName: "MedianUpdated"; + eventName: EventType.MedianUpdated; block: number; args: { token: `0x${string}`; diff --git a/src/utils/get-event-by-topic.ts b/src/utils/get-event-by-topic.ts new file mode 100644 index 0000000..0c20180 --- /dev/null +++ b/src/utils/get-event-by-topic.ts @@ -0,0 +1,14 @@ +import { EventType } from "../types"; + +export default function getEventByTopic(topic: string): EventType { + switch (topic) { + // keccak256(abi.encodePacked("ProposalCreated(uint256,address,address[],uint256[],string[],bytes[],uint256,uint256,string)")) + case "0x7d84a6263ae0d98d3329bd7b46bb4e8d6f98cd35a7adb45c274c8b7fd5ebd5e0": + return EventType.ProposalCreated; + // keccak256(abi.encodePacked("MedianUpdated(address,uint256)")) + case "0xa9981ebfc3b766a742486e898f54959b050a66006dbce1a4155c1f84a08bcf41": + return EventType.MedianUpdated; + default: + return EventType.Unknown; + } +}