From 8dea8dbf82b60aa5c88d5d9aa7403325875be5b7 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 16:18:28 +0600 Subject: [PATCH 01/24] Add event handling types and decode functionality - Introduced new types for event filtering and decoded logs in events.ts, including EventFilterOptions, DecodedEventLog, and RawLog. - Added decodeEventLog function to viem/index.ts for processing event logs. - Updated index.ts to export new event-related types for broader accessibility. --- packages/contracts/src/lib/viem/index.ts | 2 ++ packages/contracts/src/types/events.ts | 28 ++++++++++++++++++++++++ packages/contracts/src/types/index.ts | 6 ++++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/types/events.ts diff --git a/packages/contracts/src/lib/viem/index.ts b/packages/contracts/src/lib/viem/index.ts index ba047d2f..a0cf5bb7 100644 --- a/packages/contracts/src/lib/viem/index.ts +++ b/packages/contracts/src/lib/viem/index.ts @@ -9,6 +9,7 @@ export { stringToHex, encodeAbiParameters, decodeErrorResult, + decodeEventLog, parseEther, formatEther, parseUnits, @@ -23,6 +24,7 @@ export type { Address, Chain, Hex, + Log, PublicClient, WalletClient, EIP1193Provider, diff --git a/packages/contracts/src/types/events.ts b/packages/contracts/src/types/events.ts new file mode 100644 index 00000000..a42527bf --- /dev/null +++ b/packages/contracts/src/types/events.ts @@ -0,0 +1,28 @@ +import type { Hex } from "../lib"; + +/** Options for filtering historical contract event logs. */ +export interface EventFilterOptions { + /** Block number to start searching from. */ + fromBlock?: bigint; + /** Block number to stop searching at. */ + toBlock?: bigint; +} + +/** A decoded contract event log entry. */ +export interface DecodedEventLog { + /** The Solidity event name. */ + eventName: string; + /** The decoded event arguments keyed by parameter name. */ + args: Record; +} + +/** Raw log shape accepted by decodeLog helpers. */ +export interface RawLog { + /** Log topics (event signature + indexed params). */ + topics: readonly Hex[]; + /** ABI-encoded non-indexed event parameters. */ + data: Hex; +} + +/** Callback invoked when a watched event log is received. */ +export type EventWatchHandler = (logs: readonly DecodedEventLog[]) => void; diff --git a/packages/contracts/src/types/index.ts b/packages/contracts/src/types/index.ts index 39de5567..54fb1781 100644 --- a/packages/contracts/src/types/index.ts +++ b/packages/contracts/src/types/index.ts @@ -1,6 +1,10 @@ /** * Cross-contract type definitions only — no logic, no client dependencies. - * structs.ts holds on-chain struct mirrors; params.ts holds SDK-level input types. + * + * - structs.ts — on-chain struct mirrors (e.g. reward tuples, item tuples). + * - params.ts — SDK-level input types used by write/simulate methods (e.g. campaign creation params, treasury config). + * - events.ts — shared event helper types used across all contract event layers (e.g. filter options, decoded logs, watchers). */ export * from "./structs"; export * from "./params"; +export * from "./events"; From ea01f7ca12e0be15abb0c4042ceaeed5f4d4018d Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 16:19:06 +0600 Subject: [PATCH 02/24] Implement event handling for AllOrNothing contract - Added functions to fetch and decode event logs for various events in the AllOrNothing contract, including Receipt, RefundClaimed, and WithdrawalSuccessful. - Introduced a watcher mechanism for real-time event monitoring. - Updated AllOrNothingEvents interface to include new methods for retrieving and watching event logs. - Enhanced the decode functionality for raw logs to improve event handling capabilities. --- .../src/contracts/all-or-nothing/events.ts | 129 ++++++++++++++++-- .../src/contracts/all-or-nothing/types.ts | 34 ++++- 2 files changed, 154 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/contracts/all-or-nothing/events.ts b/packages/contracts/src/contracts/all-or-nothing/events.ts index 1330a58e..82b95405 100644 --- a/packages/contracts/src/contracts/all-or-nothing/events.ts +++ b/packages/contracts/src/contracts/all-or-nothing/events.ts @@ -1,18 +1,131 @@ -import type { Address, PublicClient } from "../../lib"; +import { decodeEventLog } from "../../lib"; +import type { Address, Hex, PublicClient } from "../../lib"; +import { ALL_OR_NOTHING_ABI } from "./abi"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler } from "../../types/events"; import type { AllOrNothingEvents } from "./types"; -// TODO: Add event filter factories (filterPledgeForAReward, filterWithdrawn), log decoder (decodeLog), -// and watcher factories using getLogs / watchEvent. +type AbiEventName = Extract<(typeof ALL_OR_NOTHING_ABI)[number], { type: "event" }>["name"]; + +/** + * Decodes a raw log using the AllOrNothing ABI. + * @param log - Raw log with topics and data + * @returns Decoded event name and arguments + */ +function decode(log: { topics: Hex[]; data: Hex }): DecodedEventLog { + const decoded = decodeEventLog({ abi: ALL_OR_NOTHING_ABI, topics: log.topics as [Hex, ...Hex[]], data: log.data }); + return { + eventName: decoded.eventName, + args: decoded.args as Record, + }; +} + +/** + * Fetches and decodes event logs for a specific event name. + * @param publicClient - Viem PublicClient + * @param address - Contract address + * @param eventName - ABI event name to filter by + * @param options - Optional block range + * @returns Array of decoded event logs + */ +async function fetchEventLogs( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + options?: EventFilterOptions, +): Promise { + const logs = await publicClient.getContractEvents({ + address, + abi: ALL_OR_NOTHING_ABI, + eventName, + fromBlock: options?.fromBlock ?? 0n, + toBlock: options?.toBlock, + }); + return logs.map((log) => + decode({ topics: [...log.topics] as Hex[], data: log.data }), + ); +} + +/** + * Creates a watcher for a specific event name using watchContractEvent. + * @param publicClient - Viem PublicClient + * @param address - Contract address + * @param eventName - ABI event name to watch + * @param onLogs - Handler invoked with decoded logs + * @returns Unwatch function + */ +function createWatcher( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + onLogs: EventWatchHandler, +): () => void { + return publicClient.watchContractEvent({ + address, + abi: ALL_OR_NOTHING_ABI, + eventName, + onLogs: (logs) => { + onLogs( + logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data })), + ); + }, + }); +} /** * Builds event helpers for an AllOrNothing treasury contract instance. - * @param _address - Deployed AllOrNothing contract address - * @param _publicClient - Viem PublicClient used to call getLogs + * @param address - Deployed AllOrNothing contract address + * @param publicClient - Viem PublicClient used to call getLogs * @returns Event helpers bound to the given contract address */ export function createAllOrNothingEvents( - _address: Address, - _publicClient: PublicClient, + address: Address, + publicClient: PublicClient, ): AllOrNothingEvents { - return {}; + return { + async getReceiptLogs(options) { + return fetchEventLogs(publicClient, address, "Receipt", options); + }, + async getRefundClaimedLogs(options) { + return fetchEventLogs(publicClient, address, "RefundClaimed", options); + }, + async getWithdrawalSuccessfulLogs(options) { + return fetchEventLogs(publicClient, address, "WithdrawalSuccessful", options); + }, + async getFeesDisbursedLogs(options) { + return fetchEventLogs(publicClient, address, "FeesDisbursed", options); + }, + async getRewardsAddedLogs(options) { + return fetchEventLogs(publicClient, address, "RewardsAdded", options); + }, + async getRewardRemovedLogs(options) { + return fetchEventLogs(publicClient, address, "RewardRemoved", options); + }, + async getPausedLogs(options) { + return fetchEventLogs(publicClient, address, "Paused", options); + }, + async getUnpausedLogs(options) { + return fetchEventLogs(publicClient, address, "Unpaused", options); + }, + async getTransferLogs(options) { + return fetchEventLogs(publicClient, address, "Transfer", options); + }, + async getSuccessConditionNotFulfilledLogs(options) { + return fetchEventLogs(publicClient, address, "SuccessConditionNotFulfilled", options); + }, + decodeLog(log) { + return decode({ topics: [...log.topics] as Hex[], data: log.data }); + }, + watchReceipt(onLogs) { + return createWatcher(publicClient, address, "Receipt", onLogs); + }, + watchRefundClaimed(onLogs) { + return createWatcher(publicClient, address, "RefundClaimed", onLogs); + }, + watchWithdrawalSuccessful(onLogs) { + return createWatcher(publicClient, address, "WithdrawalSuccessful", onLogs); + }, + watchFeesDisbursed(onLogs) { + return createWatcher(publicClient, address, "FeesDisbursed", onLogs); + }, + }; } diff --git a/packages/contracts/src/contracts/all-or-nothing/types.ts b/packages/contracts/src/contracts/all-or-nothing/types.ts index 52c0ce19..8dc46276 100644 --- a/packages/contracts/src/contracts/all-or-nothing/types.ts +++ b/packages/contracts/src/contracts/all-or-nothing/types.ts @@ -1,5 +1,6 @@ import type { Address, Hex } from "../../lib"; import type { TieredReward } from "../../types/structs"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler, RawLog } from "../../types/events"; import type { CallSignerOptions } from "../../client/types"; /** Read-only methods for an AllOrNothing treasury contract instance. */ @@ -107,7 +108,38 @@ export interface AllOrNothingSimulate { } /** Event helpers for an AllOrNothing treasury contract instance. */ -export interface AllOrNothingEvents {} +export interface AllOrNothingEvents { + /** Returns decoded Receipt event logs (pledge events). */ + getReceiptLogs(options?: EventFilterOptions): Promise; + /** Returns decoded RefundClaimed event logs. */ + getRefundClaimedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded WithdrawalSuccessful event logs. */ + getWithdrawalSuccessfulLogs(options?: EventFilterOptions): Promise; + /** Returns decoded FeesDisbursed event logs. */ + getFeesDisbursedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded RewardsAdded event logs. */ + getRewardsAddedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded RewardRemoved event logs. */ + getRewardRemovedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Paused event logs. */ + getPausedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Unpaused event logs. */ + getUnpausedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Transfer event logs. */ + getTransferLogs(options?: EventFilterOptions): Promise; + /** Returns decoded SuccessConditionNotFulfilled event logs. */ + getSuccessConditionNotFulfilledLogs(options?: EventFilterOptions): Promise; + /** Decodes a raw log entry against all known AllOrNothing events. */ + decodeLog(log: RawLog): DecodedEventLog; + /** Watches for Receipt events in real time. Returns an unwatch function. */ + watchReceipt(onLogs: EventWatchHandler): () => void; + /** Watches for RefundClaimed events in real time. Returns an unwatch function. */ + watchRefundClaimed(onLogs: EventWatchHandler): () => void; + /** Watches for WithdrawalSuccessful events in real time. Returns an unwatch function. */ + watchWithdrawalSuccessful(onLogs: EventWatchHandler): () => void; + /** Watches for FeesDisbursed events in real time. Returns an unwatch function. */ + watchFeesDisbursed(onLogs: EventWatchHandler): () => void; +} /** Full AllOrNothing treasury entity combining reads, writes, simulate, and events. */ export type AllOrNothingTreasuryEntity = AllOrNothingReads & AllOrNothingWrites & { From 96616b42f677a783be7818fcb64bdd7089c33737 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 16:32:08 +0600 Subject: [PATCH 03/24] Add event log retrieval and watching capabilities for Approval events in AllOrNothing - Introduced methods to fetch and decode Approval and ApprovalForAll event logs in the AllOrNothing contract. - Added watcher functions for Approval, ApprovalForAll, and several other events to enable real-time monitoring. - Updated the AllOrNothingEvents interface to include new methods for improved event handling. --- .../src/contracts/all-or-nothing/events.ts | 30 +++++++++++++++++++ .../src/contracts/all-or-nothing/types.ts | 20 +++++++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/contracts/src/contracts/all-or-nothing/events.ts b/packages/contracts/src/contracts/all-or-nothing/events.ts index 82b95405..87f690d5 100644 --- a/packages/contracts/src/contracts/all-or-nothing/events.ts +++ b/packages/contracts/src/contracts/all-or-nothing/events.ts @@ -112,6 +112,12 @@ export function createAllOrNothingEvents( async getSuccessConditionNotFulfilledLogs(options) { return fetchEventLogs(publicClient, address, "SuccessConditionNotFulfilled", options); }, + async getApprovalLogs(options) { + return fetchEventLogs(publicClient, address, "Approval", options); + }, + async getApprovalForAllLogs(options) { + return fetchEventLogs(publicClient, address, "ApprovalForAll", options); + }, decodeLog(log) { return decode({ topics: [...log.topics] as Hex[], data: log.data }); }, @@ -127,5 +133,29 @@ export function createAllOrNothingEvents( watchFeesDisbursed(onLogs) { return createWatcher(publicClient, address, "FeesDisbursed", onLogs); }, + watchRewardsAdded(onLogs) { + return createWatcher(publicClient, address, "RewardsAdded", onLogs); + }, + watchRewardRemoved(onLogs) { + return createWatcher(publicClient, address, "RewardRemoved", onLogs); + }, + watchPaused(onLogs) { + return createWatcher(publicClient, address, "Paused", onLogs); + }, + watchUnpaused(onLogs) { + return createWatcher(publicClient, address, "Unpaused", onLogs); + }, + watchTransfer(onLogs) { + return createWatcher(publicClient, address, "Transfer", onLogs); + }, + watchSuccessConditionNotFulfilled(onLogs) { + return createWatcher(publicClient, address, "SuccessConditionNotFulfilled", onLogs); + }, + watchApproval(onLogs) { + return createWatcher(publicClient, address, "Approval", onLogs); + }, + watchApprovalForAll(onLogs) { + return createWatcher(publicClient, address, "ApprovalForAll", onLogs); + }, }; } diff --git a/packages/contracts/src/contracts/all-or-nothing/types.ts b/packages/contracts/src/contracts/all-or-nothing/types.ts index 8dc46276..9e63cf66 100644 --- a/packages/contracts/src/contracts/all-or-nothing/types.ts +++ b/packages/contracts/src/contracts/all-or-nothing/types.ts @@ -129,6 +129,10 @@ export interface AllOrNothingEvents { getTransferLogs(options?: EventFilterOptions): Promise; /** Returns decoded SuccessConditionNotFulfilled event logs. */ getSuccessConditionNotFulfilledLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Approval event logs. */ + getApprovalLogs(options?: EventFilterOptions): Promise; + /** Returns decoded ApprovalForAll event logs. */ + getApprovalForAllLogs(options?: EventFilterOptions): Promise; /** Decodes a raw log entry against all known AllOrNothing events. */ decodeLog(log: RawLog): DecodedEventLog; /** Watches for Receipt events in real time. Returns an unwatch function. */ @@ -139,6 +143,22 @@ export interface AllOrNothingEvents { watchWithdrawalSuccessful(onLogs: EventWatchHandler): () => void; /** Watches for FeesDisbursed events in real time. Returns an unwatch function. */ watchFeesDisbursed(onLogs: EventWatchHandler): () => void; + /** Watches for RewardsAdded events in real time. Returns an unwatch function. */ + watchRewardsAdded(onLogs: EventWatchHandler): () => void; + /** Watches for RewardRemoved events in real time. Returns an unwatch function. */ + watchRewardRemoved(onLogs: EventWatchHandler): () => void; + /** Watches for Paused events in real time. Returns an unwatch function. */ + watchPaused(onLogs: EventWatchHandler): () => void; + /** Watches for Unpaused events in real time. Returns an unwatch function. */ + watchUnpaused(onLogs: EventWatchHandler): () => void; + /** Watches for Transfer events in real time. Returns an unwatch function. */ + watchTransfer(onLogs: EventWatchHandler): () => void; + /** Watches for SuccessConditionNotFulfilled events in real time. Returns an unwatch function. */ + watchSuccessConditionNotFulfilled(onLogs: EventWatchHandler): () => void; + /** Watches for Approval events in real time. Returns an unwatch function. */ + watchApproval(onLogs: EventWatchHandler): () => void; + /** Watches for ApprovalForAll events in real time. Returns an unwatch function. */ + watchApprovalForAll(onLogs: EventWatchHandler): () => void; } /** Full AllOrNothing treasury entity combining reads, writes, simulate, and events. */ From 282851a7ab66c2526ad61ae0c9d311647282bcc9 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:04:04 +0600 Subject: [PATCH 04/24] Implement event log handling for CampaignInfo contract - Added functions to fetch and decode event logs for various CampaignInfo events, including DeadlineUpdated, GoalAmountUpdated, LaunchTimeUpdated, PlatformInfoUpdated, SelectedPlatformUpdated, OwnershipTransferred, Paused, and Unpaused. - Introduced watcher functions for real-time monitoring of these events. - Updated CampaignInfoEvents interface to include new methods for improved event handling and log decoding capabilities. --- .../src/contracts/campaign-info/events.ts | 102 ++++++++++++++++-- .../src/contracts/campaign-info/types.ts | 38 ++++++- 2 files changed, 131 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/contracts/campaign-info/events.ts b/packages/contracts/src/contracts/campaign-info/events.ts index 8ffa2309..8620bbb9 100644 --- a/packages/contracts/src/contracts/campaign-info/events.ts +++ b/packages/contracts/src/contracts/campaign-info/events.ts @@ -1,18 +1,104 @@ -import type { Address, PublicClient } from "../../lib"; +import { decodeEventLog } from "../../lib"; +import type { Address, Hex, PublicClient } from "../../lib"; +import { CAMPAIGN_INFO_ABI } from "./abi"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler } from "../../types/events"; import type { CampaignInfoEvents } from "./types"; -// TODO: Add event filter factories (filterDeadlineUpdated, filterMintNFTForPledge), log decoder (decodeLog), -// and watcher factories using getLogs / watchEvent. +type AbiEventName = Extract<(typeof CAMPAIGN_INFO_ABI)[number], { type: "event" }>["name"]; + +function decode(log: { topics: Hex[]; data: Hex }): DecodedEventLog { + const decoded = decodeEventLog({ abi: CAMPAIGN_INFO_ABI, topics: log.topics as [Hex, ...Hex[]], data: log.data }); + return { eventName: decoded.eventName, args: decoded.args as Record }; +} + +async function fetchEventLogs( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + options?: EventFilterOptions, +): Promise { + const logs = await publicClient.getContractEvents({ + address, abi: CAMPAIGN_INFO_ABI, eventName, + fromBlock: options?.fromBlock ?? 0n, toBlock: options?.toBlock, + }); + return logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data })); +} + +function createWatcher( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + onLogs: EventWatchHandler, +): () => void { + return publicClient.watchContractEvent({ + address, abi: CAMPAIGN_INFO_ABI, eventName, + onLogs: (logs) => { + onLogs(logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data }))); + }, + }); +} /** * Builds event helpers for a CampaignInfo contract instance. - * @param _address - Deployed CampaignInfo contract address - * @param _publicClient - Viem PublicClient used to call getLogs + * @param address - Deployed CampaignInfo contract address + * @param publicClient - Viem PublicClient used to call getLogs * @returns Event helpers bound to the given contract address */ export function createCampaignInfoEvents( - _address: Address, - _publicClient: PublicClient, + address: Address, + publicClient: PublicClient, ): CampaignInfoEvents { - return {}; + return { + async getDeadlineUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "CampaignInfoDeadlineUpdated", options); + }, + async getGoalAmountUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "CampaignInfoGoalAmountUpdated", options); + }, + async getLaunchTimeUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "CampaignInfoLaunchTimeUpdated", options); + }, + async getPlatformInfoUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "CampaignInfoPlatformInfoUpdated", options); + }, + async getSelectedPlatformUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "CampaignInfoSelectedPlatformUpdated", options); + }, + async getOwnershipTransferredLogs(options) { + return fetchEventLogs(publicClient, address, "OwnershipTransferred", options); + }, + async getPausedLogs(options) { + return fetchEventLogs(publicClient, address, "Paused", options); + }, + async getUnpausedLogs(options) { + return fetchEventLogs(publicClient, address, "Unpaused", options); + }, + decodeLog(log) { + return decode({ topics: [...log.topics] as Hex[], data: log.data }); + }, + watchDeadlineUpdated(onLogs) { + return createWatcher(publicClient, address, "CampaignInfoDeadlineUpdated", onLogs); + }, + watchGoalAmountUpdated(onLogs) { + return createWatcher(publicClient, address, "CampaignInfoGoalAmountUpdated", onLogs); + }, + watchLaunchTimeUpdated(onLogs) { + return createWatcher(publicClient, address, "CampaignInfoLaunchTimeUpdated", onLogs); + }, + watchPlatformInfoUpdated(onLogs) { + return createWatcher(publicClient, address, "CampaignInfoPlatformInfoUpdated", onLogs); + }, + watchSelectedPlatformUpdated(onLogs) { + return createWatcher(publicClient, address, "CampaignInfoSelectedPlatformUpdated", onLogs); + }, + watchOwnershipTransferred(onLogs) { + return createWatcher(publicClient, address, "OwnershipTransferred", onLogs); + }, + watchPaused(onLogs) { + return createWatcher(publicClient, address, "Paused", onLogs); + }, + watchUnpaused(onLogs) { + return createWatcher(publicClient, address, "Unpaused", onLogs); + }, + }; } diff --git a/packages/contracts/src/contracts/campaign-info/types.ts b/packages/contracts/src/contracts/campaign-info/types.ts index 07e700fe..32c0bd8a 100644 --- a/packages/contracts/src/contracts/campaign-info/types.ts +++ b/packages/contracts/src/contracts/campaign-info/types.ts @@ -1,5 +1,6 @@ import type { Address, Hex } from "../../lib"; import type { LineItemTypeInfo, CampaignConfig } from "../../types/structs"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler, RawLog } from "../../types/events"; import type { CallSignerOptions } from "../../client/types"; /** Read-only methods for a CampaignInfo contract instance. */ @@ -131,7 +132,42 @@ export interface CampaignInfoSimulate { } /** Event helpers for a CampaignInfo contract instance. */ -export interface CampaignInfoEvents {} +export interface CampaignInfoEvents { + /** Returns decoded CampaignInfoDeadlineUpdated event logs. */ + getDeadlineUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded CampaignInfoGoalAmountUpdated event logs. */ + getGoalAmountUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded CampaignInfoLaunchTimeUpdated event logs. */ + getLaunchTimeUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded CampaignInfoPlatformInfoUpdated event logs. */ + getPlatformInfoUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded CampaignInfoSelectedPlatformUpdated event logs. */ + getSelectedPlatformUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded OwnershipTransferred event logs. */ + getOwnershipTransferredLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Paused event logs. */ + getPausedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Unpaused event logs. */ + getUnpausedLogs(options?: EventFilterOptions): Promise; + /** Decodes a raw log entry against all known CampaignInfo events. */ + decodeLog(log: RawLog): DecodedEventLog; + /** Watches for CampaignInfoDeadlineUpdated events in real time. Returns an unwatch function. */ + watchDeadlineUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for CampaignInfoGoalAmountUpdated events in real time. Returns an unwatch function. */ + watchGoalAmountUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for CampaignInfoLaunchTimeUpdated events in real time. Returns an unwatch function. */ + watchLaunchTimeUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for CampaignInfoPlatformInfoUpdated events in real time. Returns an unwatch function. */ + watchPlatformInfoUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for CampaignInfoSelectedPlatformUpdated events in real time. Returns an unwatch function. */ + watchSelectedPlatformUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for OwnershipTransferred events in real time. Returns an unwatch function. */ + watchOwnershipTransferred(onLogs: EventWatchHandler): () => void; + /** Watches for Paused events in real time. Returns an unwatch function. */ + watchPaused(onLogs: EventWatchHandler): () => void; + /** Watches for Unpaused events in real time. Returns an unwatch function. */ + watchUnpaused(onLogs: EventWatchHandler): () => void; +} /** Full CampaignInfo entity combining reads, writes, simulate, and events. */ export type CampaignInfoEntity = CampaignInfoReads & CampaignInfoWrites & { From 826d681b71b412100fe4191c0b7f7a57d9142133 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:04:26 +0600 Subject: [PATCH 05/24] Add event log handling for CampaignInfoFactory contract - Implemented functions to fetch and decode event logs for CampaignInfoFactory events, including CampaignCreated, CampaignInitialized, and OwnershipTransferred. - Introduced watcher functions for real-time monitoring of these events. - Updated CampaignInfoFactoryEvents interface to include new methods for improved event handling and log decoding capabilities. --- .../contracts/campaign-info-factory/events.ts | 72 ++++++++++++++++--- .../contracts/campaign-info-factory/types.ts | 18 ++++- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/contracts/campaign-info-factory/events.ts b/packages/contracts/src/contracts/campaign-info-factory/events.ts index 2dbe101a..3b906f9e 100644 --- a/packages/contracts/src/contracts/campaign-info-factory/events.ts +++ b/packages/contracts/src/contracts/campaign-info-factory/events.ts @@ -1,18 +1,74 @@ -import type { Address, PublicClient } from "../../lib"; +import { decodeEventLog } from "../../lib"; +import type { Address, Hex, PublicClient } from "../../lib"; +import { CAMPAIGN_INFO_FACTORY_ABI } from "./abi"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler } from "../../types/events"; import type { CampaignInfoFactoryEvents } from "./types"; -// TODO: Add event filter factories (filterCampaignCreated), log decoder (decodeLog), -// and watcher factories using getLogs / watchEvent. +type AbiEventName = Extract<(typeof CAMPAIGN_INFO_FACTORY_ABI)[number], { type: "event" }>["name"]; + +function decode(log: { topics: Hex[]; data: Hex }): DecodedEventLog { + const decoded = decodeEventLog({ abi: CAMPAIGN_INFO_FACTORY_ABI, topics: log.topics as [Hex, ...Hex[]], data: log.data }); + return { eventName: decoded.eventName, args: decoded.args as Record }; +} + +async function fetchEventLogs( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + options?: EventFilterOptions, +): Promise { + const logs = await publicClient.getContractEvents({ + address, abi: CAMPAIGN_INFO_FACTORY_ABI, eventName, + fromBlock: options?.fromBlock ?? 0n, toBlock: options?.toBlock, + }); + return logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data })); +} + +function createWatcher( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + onLogs: EventWatchHandler, +): () => void { + return publicClient.watchContractEvent({ + address, abi: CAMPAIGN_INFO_FACTORY_ABI, eventName, + onLogs: (logs) => { + onLogs(logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data }))); + }, + }); +} /** * Builds event helpers for a CampaignInfoFactory contract instance. - * @param _address - Deployed CampaignInfoFactory contract address - * @param _publicClient - Viem PublicClient used to call getLogs + * @param address - Deployed CampaignInfoFactory contract address + * @param publicClient - Viem PublicClient used to call getLogs * @returns Event helpers bound to the given contract address */ export function createCampaignInfoFactoryEvents( - _address: Address, - _publicClient: PublicClient, + address: Address, + publicClient: PublicClient, ): CampaignInfoFactoryEvents { - return {}; + return { + async getCampaignCreatedLogs(options) { + return fetchEventLogs(publicClient, address, "CampaignInfoFactoryCampaignCreated", options); + }, + async getCampaignInitializedLogs(options) { + return fetchEventLogs(publicClient, address, "CampaignInfoFactoryCampaignInitialized", options); + }, + async getOwnershipTransferredLogs(options) { + return fetchEventLogs(publicClient, address, "OwnershipTransferred", options); + }, + decodeLog(log) { + return decode({ topics: [...log.topics] as Hex[], data: log.data }); + }, + watchCampaignCreated(onLogs) { + return createWatcher(publicClient, address, "CampaignInfoFactoryCampaignCreated", onLogs); + }, + watchCampaignInitialized(onLogs) { + return createWatcher(publicClient, address, "CampaignInfoFactoryCampaignInitialized", onLogs); + }, + watchOwnershipTransferred(onLogs) { + return createWatcher(publicClient, address, "OwnershipTransferred", onLogs); + }, + }; } diff --git a/packages/contracts/src/contracts/campaign-info-factory/types.ts b/packages/contracts/src/contracts/campaign-info-factory/types.ts index 7cfa0774..bb4faac2 100644 --- a/packages/contracts/src/contracts/campaign-info-factory/types.ts +++ b/packages/contracts/src/contracts/campaign-info-factory/types.ts @@ -1,5 +1,6 @@ import type { Address, Hex } from "../../lib"; import type { CreateCampaignParams } from "../../types/params"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler, RawLog } from "../../types/events"; import type { CallSignerOptions } from "../../client/types"; /** Read-only methods for a CampaignInfoFactory contract instance. */ @@ -41,7 +42,22 @@ export interface CampaignInfoFactorySimulate { } /** Event helpers for a CampaignInfoFactory contract instance. */ -export interface CampaignInfoFactoryEvents {} +export interface CampaignInfoFactoryEvents { + /** Returns decoded CampaignInfoFactoryCampaignCreated event logs. */ + getCampaignCreatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded CampaignInfoFactoryCampaignInitialized event logs. */ + getCampaignInitializedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded OwnershipTransferred event logs. */ + getOwnershipTransferredLogs(options?: EventFilterOptions): Promise; + /** Decodes a raw log entry against all known CampaignInfoFactory events. */ + decodeLog(log: RawLog): DecodedEventLog; + /** Watches for CampaignInfoFactoryCampaignCreated events in real time. Returns an unwatch function. */ + watchCampaignCreated(onLogs: EventWatchHandler): () => void; + /** Watches for CampaignInfoFactoryCampaignInitialized events in real time. Returns an unwatch function. */ + watchCampaignInitialized(onLogs: EventWatchHandler): () => void; + /** Watches for OwnershipTransferred events in real time. Returns an unwatch function. */ + watchOwnershipTransferred(onLogs: EventWatchHandler): () => void; +} /** Full CampaignInfoFactory entity combining reads, writes, simulate, and events. */ export type CampaignInfoFactoryEntity = CampaignInfoFactoryReads & CampaignInfoFactoryWrites & { From 78253d69108387a252c7ec9a546077bb7b15fb2e Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:21:31 +0600 Subject: [PATCH 06/24] Add event log handling for GlobalParams contract - Implemented functions to fetch and decode event logs for various GlobalParams events, including PlatformEnlisted, PlatformDelisted, and others. - Introduced watcher functions for real-time monitoring of these events. - Updated GlobalParamsEvents interface to include new methods for improved event handling and log decoding capabilities. --- .../src/contracts/global-params/events.ts | 138 +++++++++++++++++- .../src/contracts/global-params/types.ts | 62 +++++++- 2 files changed, 191 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/contracts/global-params/events.ts b/packages/contracts/src/contracts/global-params/events.ts index ac17acc3..c6280311 100644 --- a/packages/contracts/src/contracts/global-params/events.ts +++ b/packages/contracts/src/contracts/global-params/events.ts @@ -1,18 +1,140 @@ -import type { Address, PublicClient } from "../../lib"; +import { decodeEventLog } from "../../lib"; +import type { Address, Hex, PublicClient } from "../../lib"; +import { GLOBAL_PARAMS_ABI } from "./abi"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler } from "../../types/events"; import type { GlobalParamsEvents } from "./types"; -// TODO: Add event filter factories (filterPlatformEnlisted, filterPlatformDelisted), log decoder (decodeLog), -// and watcher factories using getLogs / watchEvent. +type AbiEventName = Extract<(typeof GLOBAL_PARAMS_ABI)[number], { type: "event" }>["name"]; + +function decode(log: { topics: Hex[]; data: Hex }): DecodedEventLog { + const decoded = decodeEventLog({ abi: GLOBAL_PARAMS_ABI, topics: log.topics as [Hex, ...Hex[]], data: log.data }); + return { eventName: decoded.eventName, args: decoded.args as Record }; +} + +async function fetchEventLogs( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + options?: EventFilterOptions, +): Promise { + const logs = await publicClient.getContractEvents({ + address, abi: GLOBAL_PARAMS_ABI, eventName, + fromBlock: options?.fromBlock ?? 0n, toBlock: options?.toBlock, + }); + return logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data })); +} + +function createWatcher( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + onLogs: EventWatchHandler, +): () => void { + return publicClient.watchContractEvent({ + address, abi: GLOBAL_PARAMS_ABI, eventName, + onLogs: (logs) => { + onLogs(logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data }))); + }, + }); +} /** * Builds event helpers for a GlobalParams contract instance. - * @param _address - Deployed GlobalParams contract address - * @param _publicClient - Viem PublicClient used to call getLogs + * @param address - Deployed GlobalParams contract address + * @param publicClient - Viem PublicClient used to call getLogs * @returns Event helpers bound to the given contract address */ export function createGlobalParamsEvents( - _address: Address, - _publicClient: PublicClient, + address: Address, + publicClient: PublicClient, ): GlobalParamsEvents { - return {}; + return { + async getPlatformEnlistedLogs(options) { + return fetchEventLogs(publicClient, address, "PlatformEnlisted", options); + }, + async getPlatformDelistedLogs(options) { + return fetchEventLogs(publicClient, address, "PlatformDelisted", options); + }, + async getPlatformAdminAddressUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "PlatformAdminAddressUpdated", options); + }, + async getPlatformDataAddedLogs(options) { + return fetchEventLogs(publicClient, address, "PlatformDataAdded", options); + }, + async getPlatformDataRemovedLogs(options) { + return fetchEventLogs(publicClient, address, "PlatformDataRemoved", options); + }, + async getPlatformAdapterSetLogs(options) { + return fetchEventLogs(publicClient, address, "PlatformAdapterSet", options); + }, + async getPlatformClaimDelayUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "PlatformClaimDelayUpdated", options); + }, + async getProtocolAdminAddressUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "ProtocolAdminAddressUpdated", options); + }, + async getProtocolFeePercentUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "ProtocolFeePercentUpdated", options); + }, + async getTokenAddedToCurrencyLogs(options) { + return fetchEventLogs(publicClient, address, "TokenAddedToCurrency", options); + }, + async getTokenRemovedFromCurrencyLogs(options) { + return fetchEventLogs(publicClient, address, "TokenRemovedFromCurrency", options); + }, + async getOwnershipTransferredLogs(options) { + return fetchEventLogs(publicClient, address, "OwnershipTransferred", options); + }, + async getPausedLogs(options) { + return fetchEventLogs(publicClient, address, "Paused", options); + }, + async getUnpausedLogs(options) { + return fetchEventLogs(publicClient, address, "Unpaused", options); + }, + decodeLog(log) { + return decode({ topics: [...log.topics] as Hex[], data: log.data }); + }, + watchPlatformEnlisted(onLogs) { + return createWatcher(publicClient, address, "PlatformEnlisted", onLogs); + }, + watchPlatformDelisted(onLogs) { + return createWatcher(publicClient, address, "PlatformDelisted", onLogs); + }, + watchPlatformAdminAddressUpdated(onLogs) { + return createWatcher(publicClient, address, "PlatformAdminAddressUpdated", onLogs); + }, + watchPlatformDataAdded(onLogs) { + return createWatcher(publicClient, address, "PlatformDataAdded", onLogs); + }, + watchPlatformDataRemoved(onLogs) { + return createWatcher(publicClient, address, "PlatformDataRemoved", onLogs); + }, + watchPlatformAdapterSet(onLogs) { + return createWatcher(publicClient, address, "PlatformAdapterSet", onLogs); + }, + watchPlatformClaimDelayUpdated(onLogs) { + return createWatcher(publicClient, address, "PlatformClaimDelayUpdated", onLogs); + }, + watchProtocolAdminAddressUpdated(onLogs) { + return createWatcher(publicClient, address, "ProtocolAdminAddressUpdated", onLogs); + }, + watchProtocolFeePercentUpdated(onLogs) { + return createWatcher(publicClient, address, "ProtocolFeePercentUpdated", onLogs); + }, + watchTokenAddedToCurrency(onLogs) { + return createWatcher(publicClient, address, "TokenAddedToCurrency", onLogs); + }, + watchTokenRemovedFromCurrency(onLogs) { + return createWatcher(publicClient, address, "TokenRemovedFromCurrency", onLogs); + }, + watchOwnershipTransferred(onLogs) { + return createWatcher(publicClient, address, "OwnershipTransferred", onLogs); + }, + watchPaused(onLogs) { + return createWatcher(publicClient, address, "Paused", onLogs); + }, + watchUnpaused(onLogs) { + return createWatcher(publicClient, address, "Unpaused", onLogs); + }, + }; } diff --git a/packages/contracts/src/contracts/global-params/types.ts b/packages/contracts/src/contracts/global-params/types.ts index fe2d7c12..c23d9ac2 100644 --- a/packages/contracts/src/contracts/global-params/types.ts +++ b/packages/contracts/src/contracts/global-params/types.ts @@ -1,5 +1,6 @@ import type { Address, Hex } from "../../lib"; import type { LineItemTypeInfo } from "../../types/structs"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler, RawLog } from "../../types/events"; import type { CallSignerOptions } from "../../client/types"; /** Read-only methods for a GlobalParams contract instance. */ @@ -107,7 +108,66 @@ export interface GlobalParamsSimulate { } /** Event helpers for a GlobalParams contract instance. */ -export interface GlobalParamsEvents {} +export interface GlobalParamsEvents { + /** Returns decoded PlatformEnlisted event logs. */ + getPlatformEnlistedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PlatformDelisted event logs. */ + getPlatformDelistedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PlatformAdminAddressUpdated event logs. */ + getPlatformAdminAddressUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PlatformDataAdded event logs. */ + getPlatformDataAddedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PlatformDataRemoved event logs. */ + getPlatformDataRemovedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PlatformAdapterSet event logs. */ + getPlatformAdapterSetLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PlatformClaimDelayUpdated event logs. */ + getPlatformClaimDelayUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded ProtocolAdminAddressUpdated event logs. */ + getProtocolAdminAddressUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded ProtocolFeePercentUpdated event logs. */ + getProtocolFeePercentUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded TokenAddedToCurrency event logs. */ + getTokenAddedToCurrencyLogs(options?: EventFilterOptions): Promise; + /** Returns decoded TokenRemovedFromCurrency event logs. */ + getTokenRemovedFromCurrencyLogs(options?: EventFilterOptions): Promise; + /** Returns decoded OwnershipTransferred event logs. */ + getOwnershipTransferredLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Paused event logs. */ + getPausedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Unpaused event logs. */ + getUnpausedLogs(options?: EventFilterOptions): Promise; + /** Decodes a raw log entry against all known GlobalParams events. */ + decodeLog(log: RawLog): DecodedEventLog; + /** Watches for PlatformEnlisted events in real time. Returns an unwatch function. */ + watchPlatformEnlisted(onLogs: EventWatchHandler): () => void; + /** Watches for PlatformDelisted events in real time. Returns an unwatch function. */ + watchPlatformDelisted(onLogs: EventWatchHandler): () => void; + /** Watches for PlatformAdminAddressUpdated events in real time. Returns an unwatch function. */ + watchPlatformAdminAddressUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for PlatformDataAdded events in real time. Returns an unwatch function. */ + watchPlatformDataAdded(onLogs: EventWatchHandler): () => void; + /** Watches for PlatformDataRemoved events in real time. Returns an unwatch function. */ + watchPlatformDataRemoved(onLogs: EventWatchHandler): () => void; + /** Watches for PlatformAdapterSet events in real time. Returns an unwatch function. */ + watchPlatformAdapterSet(onLogs: EventWatchHandler): () => void; + /** Watches for PlatformClaimDelayUpdated events in real time. Returns an unwatch function. */ + watchPlatformClaimDelayUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for ProtocolAdminAddressUpdated events in real time. Returns an unwatch function. */ + watchProtocolAdminAddressUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for ProtocolFeePercentUpdated events in real time. Returns an unwatch function. */ + watchProtocolFeePercentUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for TokenAddedToCurrency events in real time. Returns an unwatch function. */ + watchTokenAddedToCurrency(onLogs: EventWatchHandler): () => void; + /** Watches for TokenRemovedFromCurrency events in real time. Returns an unwatch function. */ + watchTokenRemovedFromCurrency(onLogs: EventWatchHandler): () => void; + /** Watches for OwnershipTransferred events in real time. Returns an unwatch function. */ + watchOwnershipTransferred(onLogs: EventWatchHandler): () => void; + /** Watches for Paused events in real time. Returns an unwatch function. */ + watchPaused(onLogs: EventWatchHandler): () => void; + /** Watches for Unpaused events in real time. Returns an unwatch function. */ + watchUnpaused(onLogs: EventWatchHandler): () => void; +} /** Full GlobalParams entity combining reads, writes, simulate, and events. */ export type GlobalParamsEntity = GlobalParamsReads & GlobalParamsWrites & { From 8aecd6f039fa9f392859462b51a47332e85477d7 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:35:42 +0600 Subject: [PATCH 07/24] Add event log handling for ItemRegistry contract - Implemented functions to fetch and decode ItemAdded event logs, including a watcher for real-time monitoring. - Updated ItemRegistryEvents interface to include new methods for improved event handling and log decoding capabilities. - Enhanced the decode functionality for raw logs to support the new event structure. --- .../src/contracts/item-registry/events.ts | 60 ++++++++++++++++--- .../src/contracts/item-registry/types.ts | 10 +++- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/contracts/item-registry/events.ts b/packages/contracts/src/contracts/item-registry/events.ts index 644ad3e5..2a30c1b6 100644 --- a/packages/contracts/src/contracts/item-registry/events.ts +++ b/packages/contracts/src/contracts/item-registry/events.ts @@ -1,18 +1,62 @@ -import type { Address, PublicClient } from "../../lib"; +import { decodeEventLog } from "../../lib"; +import type { Address, Hex, PublicClient } from "../../lib"; +import { ITEM_REGISTRY_ABI } from "./abi"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler } from "../../types/events"; import type { ItemRegistryEvents } from "./types"; -// TODO: Add event filter factories (filterItemAdded), log decoder (decodeLog), -// and watcher factories using getLogs / watchEvent. +type AbiEventName = Extract<(typeof ITEM_REGISTRY_ABI)[number], { type: "event" }>["name"]; + +function decode(log: { topics: Hex[]; data: Hex }): DecodedEventLog { + const decoded = decodeEventLog({ abi: ITEM_REGISTRY_ABI, topics: log.topics as [Hex, ...Hex[]], data: log.data }); + return { eventName: decoded.eventName, args: decoded.args as Record }; +} + +async function fetchEventLogs( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + options?: EventFilterOptions, +): Promise { + const logs = await publicClient.getContractEvents({ + address, abi: ITEM_REGISTRY_ABI, eventName, + fromBlock: options?.fromBlock ?? 0n, toBlock: options?.toBlock, + }); + return logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data })); +} + +function createWatcher( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + onLogs: EventWatchHandler, +): () => void { + return publicClient.watchContractEvent({ + address, abi: ITEM_REGISTRY_ABI, eventName, + onLogs: (logs) => { + onLogs(logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data }))); + }, + }); +} /** * Builds event helpers for an ItemRegistry contract instance. - * @param _address - Deployed ItemRegistry contract address - * @param _publicClient - Viem PublicClient used to call getLogs + * @param address - Deployed ItemRegistry contract address + * @param publicClient - Viem PublicClient used to call getLogs * @returns Event helpers bound to the given contract address */ export function createItemRegistryEvents( - _address: Address, - _publicClient: PublicClient, + address: Address, + publicClient: PublicClient, ): ItemRegistryEvents { - return {}; + return { + async getItemAddedLogs(options) { + return fetchEventLogs(publicClient, address, "ItemAdded", options); + }, + decodeLog(log) { + return decode({ topics: [...log.topics] as Hex[], data: log.data }); + }, + watchItemAdded(onLogs) { + return createWatcher(publicClient, address, "ItemAdded", onLogs); + }, + }; } diff --git a/packages/contracts/src/contracts/item-registry/types.ts b/packages/contracts/src/contracts/item-registry/types.ts index 0d3a5281..8bf79d2f 100644 --- a/packages/contracts/src/contracts/item-registry/types.ts +++ b/packages/contracts/src/contracts/item-registry/types.ts @@ -1,5 +1,6 @@ import type { Address, Hex } from "../../lib"; import type { Item } from "../../types/structs"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler, RawLog } from "../../types/events"; import type { CallSignerOptions } from "../../client/types"; /** Read-only methods for ItemRegistry. */ @@ -25,7 +26,14 @@ export interface ItemRegistrySimulate { } /** Event helpers for ItemRegistry. */ -export interface ItemRegistryEvents {} +export interface ItemRegistryEvents { + /** Returns decoded ItemAdded event logs. */ + getItemAddedLogs(options?: EventFilterOptions): Promise; + /** Decodes a raw log entry against all known ItemRegistry events. */ + decodeLog(log: RawLog): DecodedEventLog; + /** Watches for ItemAdded events in real time. Returns an unwatch function. */ + watchItemAdded(onLogs: EventWatchHandler): () => void; +} /** Full ItemRegistry entity (reads, writes, simulate, events). */ export type ItemRegistryEntity = ItemRegistryReads & From 4513670931459a8dc001eb8ffef0ebef125076d5 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:36:06 +0600 Subject: [PATCH 08/24] Add event log handling for KeepWhatsRaised contract - Implemented functions to fetch and decode event logs for various KeepWhatsRaised events, including Receipt, RefundClaimed, WithdrawalWithFeeSuccessful, and others. - Introduced watcher functions for real-time monitoring of these events. - Updated KeepWhatsRaisedEvents interface to include new methods for improved event handling and log decoding capabilities. --- .../src/contracts/keep-whats-raised/events.ts | 162 +++++++++++++++++- .../src/contracts/keep-whats-raised/types.ts | 78 ++++++++- 2 files changed, 231 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/contracts/keep-whats-raised/events.ts b/packages/contracts/src/contracts/keep-whats-raised/events.ts index 2fdbf2c3..5b65a491 100644 --- a/packages/contracts/src/contracts/keep-whats-raised/events.ts +++ b/packages/contracts/src/contracts/keep-whats-raised/events.ts @@ -1,18 +1,164 @@ -import type { Address, PublicClient } from "../../lib"; +import { decodeEventLog } from "../../lib"; +import type { Address, Hex, PublicClient } from "../../lib"; +import { KEEP_WHATS_RAISED_ABI } from "./abi"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler } from "../../types/events"; import type { KeepWhatsRaisedEvents } from "./types"; -// TODO: Add event filter factories (filterPledgeMade, filterWithdrawn), log decoder (decodeLog), -// and watcher factories using getLogs / watchEvent. +type AbiEventName = Extract<(typeof KEEP_WHATS_RAISED_ABI)[number], { type: "event" }>["name"]; + +function decode(log: { topics: Hex[]; data: Hex }): DecodedEventLog { + const decoded = decodeEventLog({ abi: KEEP_WHATS_RAISED_ABI, topics: log.topics as [Hex, ...Hex[]], data: log.data }); + return { eventName: decoded.eventName, args: decoded.args as Record }; +} + +async function fetchEventLogs( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + options?: EventFilterOptions, +): Promise { + const logs = await publicClient.getContractEvents({ + address, abi: KEEP_WHATS_RAISED_ABI, eventName, + fromBlock: options?.fromBlock ?? 0n, toBlock: options?.toBlock, + }); + return logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data })); +} + +function createWatcher( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + onLogs: EventWatchHandler, +): () => void { + return publicClient.watchContractEvent({ + address, abi: KEEP_WHATS_RAISED_ABI, eventName, + onLogs: (logs) => { + onLogs(logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data }))); + }, + }); +} /** * Builds event helpers for a KeepWhatsRaised treasury contract instance. - * @param _address - Deployed KeepWhatsRaised contract address - * @param _publicClient - Viem PublicClient used to call getLogs + * @param address - Deployed KeepWhatsRaised contract address + * @param publicClient - Viem PublicClient used to call getLogs * @returns Event helpers bound to the given contract address */ export function createKeepWhatsRaisedEvents( - _address: Address, - _publicClient: PublicClient, + address: Address, + publicClient: PublicClient, ): KeepWhatsRaisedEvents { - return {}; + return { + async getReceiptLogs(options) { + return fetchEventLogs(publicClient, address, "Receipt", options); + }, + async getRefundClaimedLogs(options) { + return fetchEventLogs(publicClient, address, "RefundClaimed", options); + }, + async getWithdrawalWithFeeSuccessfulLogs(options) { + return fetchEventLogs(publicClient, address, "WithdrawalWithFeeSuccessful", options); + }, + async getWithdrawalApprovedLogs(options) { + return fetchEventLogs(publicClient, address, "WithdrawalApproved", options); + }, + async getFeesDisbursedLogs(options) { + return fetchEventLogs(publicClient, address, "FeesDisbursed", options); + }, + async getTreasuryConfiguredLogs(options) { + return fetchEventLogs(publicClient, address, "TreasuryConfigured", options); + }, + async getRewardsAddedLogs(options) { + return fetchEventLogs(publicClient, address, "RewardsAdded", options); + }, + async getRewardRemovedLogs(options) { + return fetchEventLogs(publicClient, address, "RewardRemoved", options); + }, + async getTipClaimedLogs(options) { + return fetchEventLogs(publicClient, address, "TipClaimed", options); + }, + async getFundClaimedLogs(options) { + return fetchEventLogs(publicClient, address, "FundClaimed", options); + }, + async getDeadlineUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "KeepWhatsRaisedDeadlineUpdated", options); + }, + async getGoalAmountUpdatedLogs(options) { + return fetchEventLogs(publicClient, address, "KeepWhatsRaisedGoalAmountUpdated", options); + }, + async getPaymentGatewayFeeSetLogs(options) { + return fetchEventLogs(publicClient, address, "KeepWhatsRaisedPaymentGatewayFeeSet", options); + }, + async getPausedLogs(options) { + return fetchEventLogs(publicClient, address, "Paused", options); + }, + async getUnpausedLogs(options) { + return fetchEventLogs(publicClient, address, "Unpaused", options); + }, + async getTransferLogs(options) { + return fetchEventLogs(publicClient, address, "Transfer", options); + }, + async getApprovalLogs(options) { + return fetchEventLogs(publicClient, address, "Approval", options); + }, + async getApprovalForAllLogs(options) { + return fetchEventLogs(publicClient, address, "ApprovalForAll", options); + }, + decodeLog(log) { + return decode({ topics: [...log.topics] as Hex[], data: log.data }); + }, + watchReceipt(onLogs) { + return createWatcher(publicClient, address, "Receipt", onLogs); + }, + watchRefundClaimed(onLogs) { + return createWatcher(publicClient, address, "RefundClaimed", onLogs); + }, + watchWithdrawalWithFeeSuccessful(onLogs) { + return createWatcher(publicClient, address, "WithdrawalWithFeeSuccessful", onLogs); + }, + watchWithdrawalApproved(onLogs) { + return createWatcher(publicClient, address, "WithdrawalApproved", onLogs); + }, + watchFeesDisbursed(onLogs) { + return createWatcher(publicClient, address, "FeesDisbursed", onLogs); + }, + watchTreasuryConfigured(onLogs) { + return createWatcher(publicClient, address, "TreasuryConfigured", onLogs); + }, + watchRewardsAdded(onLogs) { + return createWatcher(publicClient, address, "RewardsAdded", onLogs); + }, + watchRewardRemoved(onLogs) { + return createWatcher(publicClient, address, "RewardRemoved", onLogs); + }, + watchTipClaimed(onLogs) { + return createWatcher(publicClient, address, "TipClaimed", onLogs); + }, + watchFundClaimed(onLogs) { + return createWatcher(publicClient, address, "FundClaimed", onLogs); + }, + watchDeadlineUpdated(onLogs) { + return createWatcher(publicClient, address, "KeepWhatsRaisedDeadlineUpdated", onLogs); + }, + watchGoalAmountUpdated(onLogs) { + return createWatcher(publicClient, address, "KeepWhatsRaisedGoalAmountUpdated", onLogs); + }, + watchPaymentGatewayFeeSet(onLogs) { + return createWatcher(publicClient, address, "KeepWhatsRaisedPaymentGatewayFeeSet", onLogs); + }, + watchPaused(onLogs) { + return createWatcher(publicClient, address, "Paused", onLogs); + }, + watchUnpaused(onLogs) { + return createWatcher(publicClient, address, "Unpaused", onLogs); + }, + watchTransfer(onLogs) { + return createWatcher(publicClient, address, "Transfer", onLogs); + }, + watchApproval(onLogs) { + return createWatcher(publicClient, address, "Approval", onLogs); + }, + watchApprovalForAll(onLogs) { + return createWatcher(publicClient, address, "ApprovalForAll", onLogs); + }, + }; } diff --git a/packages/contracts/src/contracts/keep-whats-raised/types.ts b/packages/contracts/src/contracts/keep-whats-raised/types.ts index 9fa47fb5..cf395ac2 100644 --- a/packages/contracts/src/contracts/keep-whats-raised/types.ts +++ b/packages/contracts/src/contracts/keep-whats-raised/types.ts @@ -1,6 +1,7 @@ import type { Address, Hex } from "../../lib"; import type { TieredReward, CampaignData } from "../../types/structs"; import type { KeepWhatsRaisedConfig, KeepWhatsRaisedFeeKeys, KeepWhatsRaisedFeeValues } from "../../types/params"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler, RawLog } from "../../types/events"; import type { CallSignerOptions } from "../../client/types"; /** Read-only methods for KeepWhatsRaised treasury. */ @@ -210,7 +211,82 @@ export interface KeepWhatsRaisedSimulate { } /** Event helpers for KeepWhatsRaised. */ -export interface KeepWhatsRaisedEvents {} +export interface KeepWhatsRaisedEvents { + /** Returns decoded Receipt event logs (pledge events). */ + getReceiptLogs(options?: EventFilterOptions): Promise; + /** Returns decoded RefundClaimed event logs. */ + getRefundClaimedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded WithdrawalWithFeeSuccessful event logs. */ + getWithdrawalWithFeeSuccessfulLogs(options?: EventFilterOptions): Promise; + /** Returns decoded WithdrawalApproved event logs. */ + getWithdrawalApprovedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded FeesDisbursed event logs. */ + getFeesDisbursedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded TreasuryConfigured event logs. */ + getTreasuryConfiguredLogs(options?: EventFilterOptions): Promise; + /** Returns decoded RewardsAdded event logs. */ + getRewardsAddedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded RewardRemoved event logs. */ + getRewardRemovedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded TipClaimed event logs. */ + getTipClaimedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded FundClaimed event logs. */ + getFundClaimedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded KeepWhatsRaisedDeadlineUpdated event logs. */ + getDeadlineUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded KeepWhatsRaisedGoalAmountUpdated event logs. */ + getGoalAmountUpdatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded KeepWhatsRaisedPaymentGatewayFeeSet event logs. */ + getPaymentGatewayFeeSetLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Paused event logs. */ + getPausedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Unpaused event logs. */ + getUnpausedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Transfer event logs. */ + getTransferLogs(options?: EventFilterOptions): Promise; + /** Returns decoded Approval event logs. */ + getApprovalLogs(options?: EventFilterOptions): Promise; + /** Returns decoded ApprovalForAll event logs. */ + getApprovalForAllLogs(options?: EventFilterOptions): Promise; + /** Decodes a raw log entry against all known KeepWhatsRaised events. */ + decodeLog(log: RawLog): DecodedEventLog; + /** Watches for Receipt events in real time. Returns an unwatch function. */ + watchReceipt(onLogs: EventWatchHandler): () => void; + /** Watches for RefundClaimed events in real time. Returns an unwatch function. */ + watchRefundClaimed(onLogs: EventWatchHandler): () => void; + /** Watches for WithdrawalWithFeeSuccessful events in real time. Returns an unwatch function. */ + watchWithdrawalWithFeeSuccessful(onLogs: EventWatchHandler): () => void; + /** Watches for FeesDisbursed events in real time. Returns an unwatch function. */ + watchFeesDisbursed(onLogs: EventWatchHandler): () => void; + /** Watches for WithdrawalApproved events in real time. Returns an unwatch function. */ + watchWithdrawalApproved(onLogs: EventWatchHandler): () => void; + /** Watches for TreasuryConfigured events in real time. Returns an unwatch function. */ + watchTreasuryConfigured(onLogs: EventWatchHandler): () => void; + /** Watches for RewardsAdded events in real time. Returns an unwatch function. */ + watchRewardsAdded(onLogs: EventWatchHandler): () => void; + /** Watches for RewardRemoved events in real time. Returns an unwatch function. */ + watchRewardRemoved(onLogs: EventWatchHandler): () => void; + /** Watches for TipClaimed events in real time. Returns an unwatch function. */ + watchTipClaimed(onLogs: EventWatchHandler): () => void; + /** Watches for FundClaimed events in real time. Returns an unwatch function. */ + watchFundClaimed(onLogs: EventWatchHandler): () => void; + /** Watches for KeepWhatsRaisedDeadlineUpdated events in real time. Returns an unwatch function. */ + watchDeadlineUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for KeepWhatsRaisedGoalAmountUpdated events in real time. Returns an unwatch function. */ + watchGoalAmountUpdated(onLogs: EventWatchHandler): () => void; + /** Watches for KeepWhatsRaisedPaymentGatewayFeeSet events in real time. Returns an unwatch function. */ + watchPaymentGatewayFeeSet(onLogs: EventWatchHandler): () => void; + /** Watches for Paused events in real time. Returns an unwatch function. */ + watchPaused(onLogs: EventWatchHandler): () => void; + /** Watches for Unpaused events in real time. Returns an unwatch function. */ + watchUnpaused(onLogs: EventWatchHandler): () => void; + /** Watches for Transfer events in real time. Returns an unwatch function. */ + watchTransfer(onLogs: EventWatchHandler): () => void; + /** Watches for Approval events in real time. Returns an unwatch function. */ + watchApproval(onLogs: EventWatchHandler): () => void; + /** Watches for ApprovalForAll events in real time. Returns an unwatch function. */ + watchApprovalForAll(onLogs: EventWatchHandler): () => void; +} /** Full KeepWhatsRaised treasury entity (reads, writes, simulate, events). */ export type KeepWhatsRaisedTreasuryEntity = KeepWhatsRaisedReads & From 7853e50ed84c3ab0b55a27360d60bf83b77716de Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:36:22 +0600 Subject: [PATCH 09/24] Add event log handling for PaymentTreasury contract - Implemented functions to fetch and decode event logs for various PaymentTreasury events, including PaymentCreated, PaymentCancelled, PaymentConfirmed, and others. - Introduced watcher functions for real-time monitoring of these events. - Updated PaymentTreasuryEvents interface to include new methods for improved event handling and log decoding capabilities. --- .../src/contracts/payment-treasury/events.ts | 114 ++++++++++++++++-- .../src/contracts/payment-treasury/types.ts | 46 ++++++- 2 files changed, 151 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/contracts/payment-treasury/events.ts b/packages/contracts/src/contracts/payment-treasury/events.ts index 11d4b98a..7d438cf0 100644 --- a/packages/contracts/src/contracts/payment-treasury/events.ts +++ b/packages/contracts/src/contracts/payment-treasury/events.ts @@ -1,18 +1,116 @@ -import type { Address, PublicClient } from "../../lib"; +import { decodeEventLog } from "../../lib"; +import type { Address, Hex, PublicClient } from "../../lib"; +import { PAYMENT_TREASURY_ABI } from "./abi"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler } from "../../types/events"; import type { PaymentTreasuryEvents } from "./types"; -// TODO: Add event filter factories (filterPaymentMade), log decoder (decodeLog), -// and watcher factories using getLogs / watchEvent. +type AbiEventName = Extract<(typeof PAYMENT_TREASURY_ABI)[number], { type: "event" }>["name"]; + +function decode(log: { topics: Hex[]; data: Hex }): DecodedEventLog { + const decoded = decodeEventLog({ abi: PAYMENT_TREASURY_ABI, topics: log.topics as [Hex, ...Hex[]], data: log.data }); + return { eventName: decoded.eventName, args: decoded.args as Record }; +} + +async function fetchEventLogs( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + options?: EventFilterOptions, +): Promise { + const logs = await publicClient.getContractEvents({ + address, abi: PAYMENT_TREASURY_ABI, eventName, + fromBlock: options?.fromBlock ?? 0n, toBlock: options?.toBlock, + }); + return logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data })); +} + +function createWatcher( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + onLogs: EventWatchHandler, +): () => void { + return publicClient.watchContractEvent({ + address, abi: PAYMENT_TREASURY_ABI, eventName, + onLogs: (logs) => { + onLogs(logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data }))); + }, + }); +} /** * Builds event helpers for a PaymentTreasury contract instance. - * @param _address - Deployed PaymentTreasury contract address - * @param _publicClient - Viem PublicClient used to call getLogs + * @param address - Deployed PaymentTreasury contract address + * @param publicClient - Viem PublicClient used to call getLogs * @returns Event helpers bound to the given contract address */ export function createPaymentTreasuryEvents( - _address: Address, - _publicClient: PublicClient, + address: Address, + publicClient: PublicClient, ): PaymentTreasuryEvents { - return {}; + return { + async getPaymentCreatedLogs(options) { + return fetchEventLogs(publicClient, address, "PaymentCreated", options); + }, + async getPaymentCancelledLogs(options) { + return fetchEventLogs(publicClient, address, "PaymentCancelled", options); + }, + async getPaymentConfirmedLogs(options) { + return fetchEventLogs(publicClient, address, "PaymentConfirmed", options); + }, + async getPaymentBatchConfirmedLogs(options) { + return fetchEventLogs(publicClient, address, "PaymentBatchConfirmed", options); + }, + async getPaymentBatchCreatedLogs(options) { + return fetchEventLogs(publicClient, address, "PaymentBatchCreated", options); + }, + async getFeesDisbursedLogs(options) { + return fetchEventLogs(publicClient, address, "FeesDisbursed", options); + }, + async getWithdrawalWithFeeSuccessfulLogs(options) { + return fetchEventLogs(publicClient, address, "WithdrawalWithFeeSuccessful", options); + }, + async getRefundClaimedLogs(options) { + return fetchEventLogs(publicClient, address, "RefundClaimed", options); + }, + async getNonGoalLineItemsClaimedLogs(options) { + return fetchEventLogs(publicClient, address, "NonGoalLineItemsClaimed", options); + }, + async getExpiredFundsClaimedLogs(options) { + return fetchEventLogs(publicClient, address, "ExpiredFundsClaimed", options); + }, + decodeLog(log) { + return decode({ topics: [...log.topics] as Hex[], data: log.data }); + }, + watchPaymentCreated(onLogs) { + return createWatcher(publicClient, address, "PaymentCreated", onLogs); + }, + watchPaymentConfirmed(onLogs) { + return createWatcher(publicClient, address, "PaymentConfirmed", onLogs); + }, + watchPaymentCancelled(onLogs) { + return createWatcher(publicClient, address, "PaymentCancelled", onLogs); + }, + watchPaymentBatchConfirmed(onLogs) { + return createWatcher(publicClient, address, "PaymentBatchConfirmed", onLogs); + }, + watchPaymentBatchCreated(onLogs) { + return createWatcher(publicClient, address, "PaymentBatchCreated", onLogs); + }, + watchRefundClaimed(onLogs) { + return createWatcher(publicClient, address, "RefundClaimed", onLogs); + }, + watchFeesDisbursed(onLogs) { + return createWatcher(publicClient, address, "FeesDisbursed", onLogs); + }, + watchWithdrawalWithFeeSuccessful(onLogs) { + return createWatcher(publicClient, address, "WithdrawalWithFeeSuccessful", onLogs); + }, + watchNonGoalLineItemsClaimed(onLogs) { + return createWatcher(publicClient, address, "NonGoalLineItemsClaimed", onLogs); + }, + watchExpiredFundsClaimed(onLogs) { + return createWatcher(publicClient, address, "ExpiredFundsClaimed", onLogs); + }, + }; } diff --git a/packages/contracts/src/contracts/payment-treasury/types.ts b/packages/contracts/src/contracts/payment-treasury/types.ts index 9921a63f..1a83961c 100644 --- a/packages/contracts/src/contracts/payment-treasury/types.ts +++ b/packages/contracts/src/contracts/payment-treasury/types.ts @@ -1,5 +1,6 @@ import type { Address, Hex } from "../../lib"; import type { PaymentData, LineItem, ExternalFees } from "../../types/structs"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler, RawLog } from "../../types/events"; import type { CallSignerOptions } from "../../client/types"; /** Read-only methods for PaymentTreasury. */ @@ -151,7 +152,50 @@ export interface PaymentTreasurySimulate { } /** Event helpers for PaymentTreasury. */ -export interface PaymentTreasuryEvents {} +export interface PaymentTreasuryEvents { + /** Returns decoded PaymentCreated event logs. */ + getPaymentCreatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PaymentCancelled event logs. */ + getPaymentCancelledLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PaymentConfirmed event logs. */ + getPaymentConfirmedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PaymentBatchConfirmed event logs. */ + getPaymentBatchConfirmedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded PaymentBatchCreated event logs. */ + getPaymentBatchCreatedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded FeesDisbursed event logs. */ + getFeesDisbursedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded WithdrawalWithFeeSuccessful event logs. */ + getWithdrawalWithFeeSuccessfulLogs(options?: EventFilterOptions): Promise; + /** Returns decoded RefundClaimed event logs. */ + getRefundClaimedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded NonGoalLineItemsClaimed event logs. */ + getNonGoalLineItemsClaimedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded ExpiredFundsClaimed event logs. */ + getExpiredFundsClaimedLogs(options?: EventFilterOptions): Promise; + /** Decodes a raw log entry against all known PaymentTreasury events. */ + decodeLog(log: RawLog): DecodedEventLog; + /** Watches for PaymentCreated events in real time. Returns an unwatch function. */ + watchPaymentCreated(onLogs: EventWatchHandler): () => void; + /** Watches for PaymentConfirmed events in real time. Returns an unwatch function. */ + watchPaymentConfirmed(onLogs: EventWatchHandler): () => void; + /** Watches for PaymentCancelled events in real time. Returns an unwatch function. */ + watchPaymentCancelled(onLogs: EventWatchHandler): () => void; + /** Watches for PaymentBatchConfirmed events in real time. Returns an unwatch function. */ + watchPaymentBatchConfirmed(onLogs: EventWatchHandler): () => void; + /** Watches for PaymentBatchCreated events in real time. Returns an unwatch function. */ + watchPaymentBatchCreated(onLogs: EventWatchHandler): () => void; + /** Watches for RefundClaimed events in real time. Returns an unwatch function. */ + watchRefundClaimed(onLogs: EventWatchHandler): () => void; + /** Watches for FeesDisbursed events in real time. Returns an unwatch function. */ + watchFeesDisbursed(onLogs: EventWatchHandler): () => void; + /** Watches for WithdrawalWithFeeSuccessful events in real time. Returns an unwatch function. */ + watchWithdrawalWithFeeSuccessful(onLogs: EventWatchHandler): () => void; + /** Watches for NonGoalLineItemsClaimed events in real time. Returns an unwatch function. */ + watchNonGoalLineItemsClaimed(onLogs: EventWatchHandler): () => void; + /** Watches for ExpiredFundsClaimed events in real time. Returns an unwatch function. */ + watchExpiredFundsClaimed(onLogs: EventWatchHandler): () => void; +} /** * Full PaymentTreasury entity (reads, writes, simulate, events). From 7301965624cfa2658a05ccc8c84e31bd9c7ccbb4 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:38:30 +0600 Subject: [PATCH 10/24] Add event log handling for TreasuryFactory contract - Implemented functions to fetch and decode event logs for TreasuryFactory events, including TreasuryFactoryTreasuryDeployed, TreasuryImplementationRegistered, TreasuryImplementationRemoved, and TreasuryImplementationApproval. - Introduced watcher functions for real-time monitoring of these events. - Updated TreasuryFactoryEvents interface to include new methods for improved event handling and log decoding capabilities. --- .../src/contracts/treasury-factory/events.ts | 78 +++++++++++++++++-- .../src/contracts/treasury-factory/types.ts | 22 +++++- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/contracts/treasury-factory/events.ts b/packages/contracts/src/contracts/treasury-factory/events.ts index 49670f1a..0c83d7e2 100644 --- a/packages/contracts/src/contracts/treasury-factory/events.ts +++ b/packages/contracts/src/contracts/treasury-factory/events.ts @@ -1,18 +1,80 @@ -import type { Address, PublicClient } from "../../lib"; +import { decodeEventLog } from "../../lib"; +import type { Address, Hex, PublicClient } from "../../lib"; +import { TREASURY_FACTORY_ABI } from "./abi"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler } from "../../types/events"; import type { TreasuryFactoryEvents } from "./types"; -// TODO: Add event filter factories (filterTreasuryDeployed), log decoder (decodeLog), -// and watcher factories using getLogs / watchEvent. +type AbiEventName = Extract<(typeof TREASURY_FACTORY_ABI)[number], { type: "event" }>["name"]; + +function decode(log: { topics: Hex[]; data: Hex }): DecodedEventLog { + const decoded = decodeEventLog({ abi: TREASURY_FACTORY_ABI, topics: log.topics as [Hex, ...Hex[]], data: log.data }); + return { eventName: decoded.eventName, args: decoded.args as Record }; +} + +async function fetchEventLogs( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + options?: EventFilterOptions, +): Promise { + const logs = await publicClient.getContractEvents({ + address, abi: TREASURY_FACTORY_ABI, eventName, + fromBlock: options?.fromBlock ?? 0n, toBlock: options?.toBlock, + }); + return logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data })); +} + +function createWatcher( + publicClient: PublicClient, + address: Address, + eventName: AbiEventName, + onLogs: EventWatchHandler, +): () => void { + return publicClient.watchContractEvent({ + address, abi: TREASURY_FACTORY_ABI, eventName, + onLogs: (logs) => { + onLogs(logs.map((log) => decode({ topics: [...log.topics] as Hex[], data: log.data }))); + }, + }); +} /** * Builds event helpers for a TreasuryFactory contract instance. - * @param _address - Deployed TreasuryFactory contract address - * @param _publicClient - Viem PublicClient used to call getLogs + * @param address - Deployed TreasuryFactory contract address + * @param publicClient - Viem PublicClient used to call getLogs * @returns Event helpers bound to the given contract address */ export function createTreasuryFactoryEvents( - _address: Address, - _publicClient: PublicClient, + address: Address, + publicClient: PublicClient, ): TreasuryFactoryEvents { - return {}; + return { + async getTreasuryDeployedLogs(options) { + return fetchEventLogs(publicClient, address, "TreasuryFactoryTreasuryDeployed", options); + }, + async getImplementationRegisteredLogs(options) { + return fetchEventLogs(publicClient, address, "TreasuryImplementationRegistered", options); + }, + async getImplementationRemovedLogs(options) { + return fetchEventLogs(publicClient, address, "TreasuryImplementationRemoved", options); + }, + async getImplementationApprovalLogs(options) { + return fetchEventLogs(publicClient, address, "TreasuryImplementationApproval", options); + }, + decodeLog(log) { + return decode({ topics: [...log.topics] as Hex[], data: log.data }); + }, + watchTreasuryDeployed(onLogs) { + return createWatcher(publicClient, address, "TreasuryFactoryTreasuryDeployed", onLogs); + }, + watchImplementationRegistered(onLogs) { + return createWatcher(publicClient, address, "TreasuryImplementationRegistered", onLogs); + }, + watchImplementationRemoved(onLogs) { + return createWatcher(publicClient, address, "TreasuryImplementationRemoved", onLogs); + }, + watchImplementationApproval(onLogs) { + return createWatcher(publicClient, address, "TreasuryImplementationApproval", onLogs); + }, + }; } diff --git a/packages/contracts/src/contracts/treasury-factory/types.ts b/packages/contracts/src/contracts/treasury-factory/types.ts index c41679f8..3c37770d 100644 --- a/packages/contracts/src/contracts/treasury-factory/types.ts +++ b/packages/contracts/src/contracts/treasury-factory/types.ts @@ -1,4 +1,5 @@ import type { Address, Hex } from "../../lib"; +import type { DecodedEventLog, EventFilterOptions, EventWatchHandler, RawLog } from "../../types/events"; import type { CallSignerOptions } from "../../client/types"; /** Read-only methods for TreasuryFactory (none in ABI). */ @@ -33,7 +34,26 @@ export interface TreasuryFactorySimulate { } /** Event helpers for a TreasuryFactory contract instance. */ -export interface TreasuryFactoryEvents {} +export interface TreasuryFactoryEvents { + /** Returns decoded TreasuryFactoryTreasuryDeployed event logs. */ + getTreasuryDeployedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded TreasuryImplementationRegistered event logs. */ + getImplementationRegisteredLogs(options?: EventFilterOptions): Promise; + /** Returns decoded TreasuryImplementationRemoved event logs. */ + getImplementationRemovedLogs(options?: EventFilterOptions): Promise; + /** Returns decoded TreasuryImplementationApproval event logs. */ + getImplementationApprovalLogs(options?: EventFilterOptions): Promise; + /** Decodes a raw log entry against all known TreasuryFactory events. */ + decodeLog(log: RawLog): DecodedEventLog; + /** Watches for TreasuryFactoryTreasuryDeployed events in real time. Returns an unwatch function. */ + watchTreasuryDeployed(onLogs: EventWatchHandler): () => void; + /** Watches for TreasuryImplementationRegistered events in real time. Returns an unwatch function. */ + watchImplementationRegistered(onLogs: EventWatchHandler): () => void; + /** Watches for TreasuryImplementationRemoved events in real time. Returns an unwatch function. */ + watchImplementationRemoved(onLogs: EventWatchHandler): () => void; + /** Watches for TreasuryImplementationApproval events in real time. Returns an unwatch function. */ + watchImplementationApproval(onLogs: EventWatchHandler): () => void; +} /** Full TreasuryFactory entity combining reads, writes, simulate, and events. */ export type TreasuryFactoryEntity = TreasuryFactoryReads & From 002215abdf5b23fef7b961337a250980918fc354 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:47:29 +0600 Subject: [PATCH 11/24] Update Jest configuration to increase test timeout to 60 seconds - Added a testTimeout property to the Jest configuration to allow for longer-running tests, improving test reliability in scenarios that require more time to complete. --- packages/contracts/jest.config.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/contracts/jest.config.cjs b/packages/contracts/jest.config.cjs index 5dd2b55a..06f9825a 100644 --- a/packages/contracts/jest.config.cjs +++ b/packages/contracts/jest.config.cjs @@ -1,6 +1,7 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", + testTimeout: 60_000, testMatch: ["**/__tests__/**/*.test.ts"], setupFiles: ["dotenv/config"], collectCoverageFrom: [ From e17f5e4fffb51e8dc881e7eb325d353b7332cacb Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:50:05 +0600 Subject: [PATCH 12/24] Updated event tests for all contracts - Updated event tests for AllOrNothing, CampaignInfo, CampaignInfoFactory, GlobalParams, KeepWhatsRaised, PaymentTreasury, and TreasuryFactory contracts to verify the presence of event helper methods. --- .../integration/all-or-nothing.test.ts | 6 +- .../integration/campaign-info-factory.test.ts | 6 +- .../integration/campaign-info.test.ts | 6 +- .../integration/global-params.test.ts | 6 +- .../integration/keep-whats-raised.test.ts | 6 +- .../integration/payment-treasury.test.ts | 6 +- .../integration/treasury-factory.test.ts | 6 +- .../__tests__/unit/contract-entities.test.ts | 383 +++++++++++++++++- 8 files changed, 403 insertions(+), 22 deletions(-) diff --git a/packages/contracts/__tests__/integration/all-or-nothing.test.ts b/packages/contracts/__tests__/integration/all-or-nothing.test.ts index 32c78e90..aacdcea1 100644 --- a/packages/contracts/__tests__/integration/all-or-nothing.test.ts +++ b/packages/contracts/__tests__/integration/all-or-nothing.test.ts @@ -56,7 +56,9 @@ describe("AllOrNothing — simulate (may throw)", () => { }); describe("AllOrNothing — events", () => { - it("events is an empty object", () => { - expect(aon.events).toEqual({}); + it("events exposes event helpers", () => { + expect(typeof aon.events.getReceiptLogs).toBe("function"); + expect(typeof aon.events.decodeLog).toBe("function"); + expect(typeof aon.events.watchReceipt).toBe("function"); }); }); diff --git a/packages/contracts/__tests__/integration/campaign-info-factory.test.ts b/packages/contracts/__tests__/integration/campaign-info-factory.test.ts index ba635656..3f6b035d 100644 --- a/packages/contracts/__tests__/integration/campaign-info-factory.test.ts +++ b/packages/contracts/__tests__/integration/campaign-info-factory.test.ts @@ -126,7 +126,9 @@ describe("CampaignInfoFactory — simulate (may throw)", () => { }); describe("CampaignInfoFactory — events", () => { - it("events is an empty object", () => { - expect(cif.events).toEqual({}); + it("events exposes event helpers", () => { + expect(typeof cif.events.getCampaignCreatedLogs).toBe("function"); + expect(typeof cif.events.decodeLog).toBe("function"); + expect(typeof cif.events.watchCampaignCreated).toBe("function"); }); }); diff --git a/packages/contracts/__tests__/integration/campaign-info.test.ts b/packages/contracts/__tests__/integration/campaign-info.test.ts index 4cae9d0b..f7bcc72b 100644 --- a/packages/contracts/__tests__/integration/campaign-info.test.ts +++ b/packages/contracts/__tests__/integration/campaign-info.test.ts @@ -127,7 +127,9 @@ describe("CampaignInfo — simulate (may throw)", () => { }); describe("CampaignInfo — events", () => { - it("events is an empty object", () => { - expect(ci.events).toEqual({}); + it("events exposes event helpers", () => { + expect(typeof ci.events.getDeadlineUpdatedLogs).toBe("function"); + expect(typeof ci.events.decodeLog).toBe("function"); + expect(typeof ci.events.watchDeadlineUpdated).toBe("function"); }); }); diff --git a/packages/contracts/__tests__/integration/global-params.test.ts b/packages/contracts/__tests__/integration/global-params.test.ts index dc350572..4b5293e8 100644 --- a/packages/contracts/__tests__/integration/global-params.test.ts +++ b/packages/contracts/__tests__/integration/global-params.test.ts @@ -270,7 +270,9 @@ describe("GlobalParams — simulate (may throw)", () => { }); describe("GlobalParams — events", () => { - it("events is an empty object", () => { - expect(gp.events).toEqual({}); + it("events exposes event helpers", () => { + expect(typeof gp.events.getPlatformEnlistedLogs).toBe("function"); + expect(typeof gp.events.decodeLog).toBe("function"); + expect(typeof gp.events.watchPlatformEnlisted).toBe("function"); }); }); diff --git a/packages/contracts/__tests__/integration/keep-whats-raised.test.ts b/packages/contracts/__tests__/integration/keep-whats-raised.test.ts index 2dc596d6..7af452ae 100644 --- a/packages/contracts/__tests__/integration/keep-whats-raised.test.ts +++ b/packages/contracts/__tests__/integration/keep-whats-raised.test.ts @@ -78,7 +78,9 @@ describe("KeepWhatsRaised — simulate (may throw)", () => { }); describe("KeepWhatsRaised — events", () => { - it("events is an empty object", () => { - expect(kwr.events).toEqual({}); + it("events exposes event helpers", () => { + expect(typeof kwr.events.getReceiptLogs).toBe("function"); + expect(typeof kwr.events.decodeLog).toBe("function"); + expect(typeof kwr.events.watchReceipt).toBe("function"); }); }); diff --git a/packages/contracts/__tests__/integration/payment-treasury.test.ts b/packages/contracts/__tests__/integration/payment-treasury.test.ts index 86ae5cd8..8076115a 100644 --- a/packages/contracts/__tests__/integration/payment-treasury.test.ts +++ b/packages/contracts/__tests__/integration/payment-treasury.test.ts @@ -55,7 +55,9 @@ describe("PaymentTreasury — simulate (may throw)", () => { }); describe("PaymentTreasury — events", () => { - it("events is an empty object", () => { - expect(pt.events).toEqual({}); + it("events exposes event helpers", () => { + expect(typeof pt.events.getPaymentCreatedLogs).toBe("function"); + expect(typeof pt.events.decodeLog).toBe("function"); + expect(typeof pt.events.watchPaymentCreated).toBe("function"); }); }); diff --git a/packages/contracts/__tests__/integration/treasury-factory.test.ts b/packages/contracts/__tests__/integration/treasury-factory.test.ts index 21706d6e..c3f177a9 100644 --- a/packages/contracts/__tests__/integration/treasury-factory.test.ts +++ b/packages/contracts/__tests__/integration/treasury-factory.test.ts @@ -110,7 +110,9 @@ describe("TreasuryFactory — simulate (may throw)", () => { }); describe("TreasuryFactory — events", () => { - it("events is an empty object", () => { - expect(tf.events).toEqual({}); + it("events exposes event helpers", () => { + expect(typeof tf.events.getTreasuryDeployedLogs).toBe("function"); + expect(typeof tf.events.decodeLog).toBe("function"); + expect(typeof tf.events.watchTreasuryDeployed).toBe("function"); }); }); diff --git a/packages/contracts/__tests__/unit/contract-entities.test.ts b/packages/contracts/__tests__/unit/contract-entities.test.ts index 6837e16e..dd00d863 100644 --- a/packages/contracts/__tests__/unit/contract-entities.test.ts +++ b/packages/contracts/__tests__/unit/contract-entities.test.ts @@ -5,14 +5,19 @@ */ import type { Address, PublicClient, WalletClient, Chain } from "../../src/lib"; +import { keccak256, toHex } from "viem"; const ADDR = "0x0000000000000000000000000000000000000001" as Address; const B32 = ("0x" + "00".repeat(32)) as `0x${string}`; +type WatchContractEventArgs = { onLogs: (logs: unknown[]) => void }; + function mockPublicClient(): PublicClient { return { readContract: jest.fn().mockResolvedValue(0n), simulateContract: jest.fn().mockResolvedValue({ result: undefined }), + getContractEvents: jest.fn().mockResolvedValue([]), + watchContractEvent: jest.fn().mockImplementation((_args: WatchContractEventArgs) => () => {}), } as unknown as PublicClient; } @@ -90,7 +95,70 @@ describe("GlobalParams entity", () => { it("renounceOwnership", async () => { await entity.simulate.renounceOwnership(); }); }); - it("events is empty", () => { expect(entity.events).toEqual({}); }); + describe("events", () => { + it("exposes event methods", () => { + expect(typeof entity.events.getPlatformEnlistedLogs).toBe("function"); + expect(typeof entity.events.getPlatformDelistedLogs).toBe("function"); + expect(typeof entity.events.decodeLog).toBe("function"); + expect(typeof entity.events.watchPlatformEnlisted).toBe("function"); + }); + it("getPlatformEnlistedLogs", async () => { await entity.events.getPlatformEnlistedLogs(); expect(pub.getContractEvents).toHaveBeenCalled(); }); + it("getPlatformDelistedLogs", async () => { await entity.events.getPlatformDelistedLogs(); }); + it("getPlatformAdminAddressUpdatedLogs", async () => { await entity.events.getPlatformAdminAddressUpdatedLogs(); }); + it("getPlatformDataAddedLogs", async () => { await entity.events.getPlatformDataAddedLogs(); }); + it("getPlatformDataRemovedLogs", async () => { await entity.events.getPlatformDataRemovedLogs(); }); + it("getPlatformAdapterSetLogs", async () => { await entity.events.getPlatformAdapterSetLogs(); }); + it("getPlatformClaimDelayUpdatedLogs", async () => { await entity.events.getPlatformClaimDelayUpdatedLogs(); }); + it("getProtocolAdminAddressUpdatedLogs", async () => { await entity.events.getProtocolAdminAddressUpdatedLogs(); }); + it("getProtocolFeePercentUpdatedLogs", async () => { await entity.events.getProtocolFeePercentUpdatedLogs(); }); + it("getTokenAddedToCurrencyLogs", async () => { await entity.events.getTokenAddedToCurrencyLogs(); }); + it("getTokenRemovedFromCurrencyLogs", async () => { await entity.events.getTokenRemovedFromCurrencyLogs(); }); + it("getOwnershipTransferredLogs", async () => { await entity.events.getOwnershipTransferredLogs(); }); + it("getPausedLogs", async () => { await entity.events.getPausedLogs(); }); + it("getUnpausedLogs", async () => { await entity.events.getUnpausedLogs(); }); + it("watchPlatformEnlisted", () => { entity.events.watchPlatformEnlisted(() => {}); expect(pub.watchContractEvent).toHaveBeenCalled(); }); + it("watchPlatformDelisted", () => { entity.events.watchPlatformDelisted(() => {}); }); + it("watchPlatformAdminAddressUpdated", () => { entity.events.watchPlatformAdminAddressUpdated(() => {}); }); + it("watchPlatformDataAdded", () => { entity.events.watchPlatformDataAdded(() => {}); }); + it("watchPlatformDataRemoved", () => { entity.events.watchPlatformDataRemoved(() => {}); }); + it("watchPlatformAdapterSet", () => { entity.events.watchPlatformAdapterSet(() => {}); }); + it("watchPlatformClaimDelayUpdated", () => { entity.events.watchPlatformClaimDelayUpdated(() => {}); }); + it("watchProtocolAdminAddressUpdated", () => { entity.events.watchProtocolAdminAddressUpdated(() => {}); }); + it("watchProtocolFeePercentUpdated", () => { entity.events.watchProtocolFeePercentUpdated(() => {}); }); + it("watchTokenAddedToCurrency", () => { entity.events.watchTokenAddedToCurrency(() => {}); }); + it("watchTokenRemovedFromCurrency", () => { entity.events.watchTokenRemovedFromCurrency(() => {}); }); + it("watchOwnershipTransferred", () => { entity.events.watchOwnershipTransferred(() => {}); }); + it("watchPaused", () => { entity.events.watchPaused(() => {}); }); + it("watchUnpaused", () => { entity.events.watchUnpaused(() => {}); }); + it("decodeLog decodes a Paused event", () => { + const pausedSig = keccak256(toHex("Paused(address)")); + const result = entity.events.decodeLog({ + topics: [pausedSig], + data: ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}`, + }); + expect(result.eventName).toBe("Paused"); + }); + it("getLogs with fromBlock/toBlock options", async () => { + await entity.events.getPlatformEnlistedLogs({ fromBlock: 0n, toBlock: 100n }); + expect(pub.getContractEvents).toHaveBeenCalledWith(expect.objectContaining({ fromBlock: 0n, toBlock: 100n })); + }); + it("watcher callback invokes handler with decoded logs", () => { + const captured: WatchContractEventArgs[] = []; + (pub.watchContractEvent as jest.Mock).mockImplementation((args: WatchContractEventArgs) => { captured.push(args); return () => {}; }); + const handler = jest.fn(); + entity.events.watchPlatformEnlisted(handler); + const pausedSig = keccak256(toHex("Paused(address)")); + captured[0].onLogs([{ topics: [pausedSig], data: ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}` }]); + expect(handler).toHaveBeenCalledWith(expect.arrayContaining([expect.objectContaining({ eventName: "Paused" })])); + }); + it("fetchEventLogs decodes returned logs", async () => { + const pausedSig = keccak256(toHex("Paused(address)")); + (pub.getContractEvents as jest.Mock).mockResolvedValueOnce([{ topics: [pausedSig], data: ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}` }]); + const logs = await entity.events.getPausedLogs(); + expect(logs).toHaveLength(1); + expect(logs[0].eventName).toBe("Paused"); + }); + }); }); // ============================================================ @@ -126,7 +194,34 @@ describe("CampaignInfoFactory entity", () => { it("simulate.updateImplementation", async () => { await entity.simulate.updateImplementation(ADDR); }); it("simulate.transferOwnership", async () => { await entity.simulate.transferOwnership(ADDR); }); it("simulate.renounceOwnership", async () => { await entity.simulate.renounceOwnership(); }); - it("events is empty", () => { expect(entity.events).toEqual({}); }); + describe("events", () => { + it("getCampaignCreatedLogs", async () => { await entity.events.getCampaignCreatedLogs(); expect(pub.getContractEvents).toHaveBeenCalled(); }); + it("getCampaignInitializedLogs", async () => { await entity.events.getCampaignInitializedLogs(); }); + it("getOwnershipTransferredLogs", async () => { await entity.events.getOwnershipTransferredLogs(); }); + it("watchCampaignCreated", () => { entity.events.watchCampaignCreated(() => {}); expect(pub.watchContractEvent).toHaveBeenCalled(); }); + it("watchCampaignInitialized", () => { entity.events.watchCampaignInitialized(() => {}); }); + it("watchOwnershipTransferred", () => { entity.events.watchOwnershipTransferred(() => {}); }); + it("decodeLog decodes a CampaignInfoFactoryCampaignInitialized event", () => { + const sig = keccak256(toHex("CampaignInfoFactoryCampaignInitialized()")); + const result = entity.events.decodeLog({ topics: [sig], data: "0x" as `0x${string}` }); + expect(result.eventName).toBe("CampaignInfoFactoryCampaignInitialized"); + }); + it("fetchEventLogs decodes returned logs", async () => { + const sig = keccak256(toHex("CampaignInfoFactoryCampaignInitialized()")); + (pub.getContractEvents as jest.Mock).mockResolvedValueOnce([{ topics: [sig], data: "0x" as `0x${string}` }]); + const logs = await entity.events.getCampaignInitializedLogs(); + expect(logs).toHaveLength(1); + }); + it("watcher callback invokes handler", () => { + const captured: WatchContractEventArgs[] = []; + (pub.watchContractEvent as jest.Mock).mockImplementation((args: WatchContractEventArgs) => { captured.push(args); return () => {}; }); + const handler = jest.fn(); + entity.events.watchCampaignCreated(handler); + const sig = keccak256(toHex("CampaignInfoFactoryCampaignInitialized()")); + captured[0].onLogs([{ topics: [sig], data: "0x" as `0x${string}` }]); + expect(handler).toHaveBeenCalled(); + }); + }); }); // ============================================================ @@ -149,7 +244,42 @@ describe("TreasuryFactory entity", () => { it("simulate.approveTreasuryImplementation", async () => { await entity.simulate.approveTreasuryImplementation(B32, 0n); }); it("simulate.disapproveTreasuryImplementation", async () => { await entity.simulate.disapproveTreasuryImplementation(ADDR); }); it("simulate.removeTreasuryImplementation", async () => { await entity.simulate.removeTreasuryImplementation(B32, 0n); }); - it("events is empty", () => { expect(entity.events).toEqual({}); }); + describe("events", () => { + it("getTreasuryDeployedLogs", async () => { await entity.events.getTreasuryDeployedLogs(); expect(pub.getContractEvents).toHaveBeenCalled(); }); + it("getImplementationRegisteredLogs", async () => { await entity.events.getImplementationRegisteredLogs(); }); + it("getImplementationRemovedLogs", async () => { await entity.events.getImplementationRemovedLogs(); }); + it("getImplementationApprovalLogs", async () => { await entity.events.getImplementationApprovalLogs(); }); + it("watchTreasuryDeployed", () => { entity.events.watchTreasuryDeployed(() => {}); expect(pub.watchContractEvent).toHaveBeenCalled(); }); + it("watchImplementationRegistered", () => { entity.events.watchImplementationRegistered(() => {}); }); + it("watchImplementationRemoved", () => { entity.events.watchImplementationRemoved(() => {}); }); + it("watchImplementationApproval", () => { entity.events.watchImplementationApproval(() => {}); }); + it("decodeLog decodes a TreasuryImplementationApproval event", () => { + const sig = keccak256(toHex("TreasuryImplementationApproval(address,bool)")); + const implTopic = ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}`; + const data = ("0x" + "0".repeat(63) + "1") as `0x${string}`; + const result = entity.events.decodeLog({ topics: [sig, implTopic], data }); + expect(result.eventName).toBe("TreasuryImplementationApproval"); + }); + it("fetchEventLogs decodes returned logs", async () => { + const sig = keccak256(toHex("TreasuryImplementationApproval(address,bool)")); + const implTopic = ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}`; + const data = ("0x" + "0".repeat(63) + "1") as `0x${string}`; + (pub.getContractEvents as jest.Mock).mockResolvedValueOnce([{ topics: [sig, implTopic], data }]); + const logs = await entity.events.getImplementationApprovalLogs(); + expect(logs).toHaveLength(1); + }); + it("watcher callback invokes handler", () => { + const captured: WatchContractEventArgs[] = []; + (pub.watchContractEvent as jest.Mock).mockImplementation((args: WatchContractEventArgs) => { captured.push(args); return () => {}; }); + const handler = jest.fn(); + entity.events.watchTreasuryDeployed(handler); + const sig = keccak256(toHex("TreasuryImplementationApproval(address,bool)")); + const implTopic = ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}`; + const data = ("0x" + "0".repeat(63) + "1") as `0x${string}`; + captured[0].onLogs([{ topics: [sig, implTopic], data }]); + expect(handler).toHaveBeenCalled(); + }); + }); }); // ============================================================ @@ -229,7 +359,52 @@ describe("CampaignInfo entity", () => { it("renounceOwnership", async () => { await entity.simulate.renounceOwnership(); }); }); - it("events is empty", () => { expect(entity.events).toEqual({}); }); + describe("events", () => { + it("getDeadlineUpdatedLogs", async () => { await entity.events.getDeadlineUpdatedLogs(); expect(pub.getContractEvents).toHaveBeenCalled(); }); + it("getGoalAmountUpdatedLogs", async () => { await entity.events.getGoalAmountUpdatedLogs(); }); + it("getLaunchTimeUpdatedLogs", async () => { await entity.events.getLaunchTimeUpdatedLogs(); }); + it("getPlatformInfoUpdatedLogs", async () => { await entity.events.getPlatformInfoUpdatedLogs(); }); + it("getSelectedPlatformUpdatedLogs", async () => { await entity.events.getSelectedPlatformUpdatedLogs(); }); + it("getOwnershipTransferredLogs", async () => { await entity.events.getOwnershipTransferredLogs(); }); + it("getPausedLogs", async () => { await entity.events.getPausedLogs(); }); + it("getUnpausedLogs", async () => { await entity.events.getUnpausedLogs(); }); + it("watchDeadlineUpdated", () => { entity.events.watchDeadlineUpdated(() => {}); expect(pub.watchContractEvent).toHaveBeenCalled(); }); + it("watchGoalAmountUpdated", () => { entity.events.watchGoalAmountUpdated(() => {}); }); + it("watchLaunchTimeUpdated", () => { entity.events.watchLaunchTimeUpdated(() => {}); }); + it("watchPlatformInfoUpdated", () => { entity.events.watchPlatformInfoUpdated(() => {}); }); + it("watchSelectedPlatformUpdated", () => { entity.events.watchSelectedPlatformUpdated(() => {}); }); + it("watchOwnershipTransferred", () => { entity.events.watchOwnershipTransferred(() => {}); }); + it("watchPaused", () => { entity.events.watchPaused(() => {}); }); + it("watchUnpaused", () => { entity.events.watchUnpaused(() => {}); }); + it("decodeLog decodes a CampaignInfoDeadlineUpdated event", () => { + const sig = keccak256(toHex("CampaignInfoDeadlineUpdated(uint256)")); + const data = ("0x" + "0".repeat(63) + "1") as `0x${string}`; + const result = entity.events.decodeLog({ topics: [sig], data }); + expect(result.eventName).toBe("CampaignInfoDeadlineUpdated"); + }); + it("fetchEventLogs decodes returned logs", async () => { + const sig = keccak256(toHex("CampaignInfoDeadlineUpdated(uint256)")); + const data = ("0x" + "0".repeat(63) + "1") as `0x${string}`; + (pub.getContractEvents as jest.Mock).mockResolvedValueOnce([{ topics: [sig], data }]); + const logs = await entity.events.getDeadlineUpdatedLogs(); + expect(logs).toHaveLength(1); + expect(logs[0].eventName).toBe("CampaignInfoDeadlineUpdated"); + }); + it("watcher callback invokes handler", () => { + const captured: WatchContractEventArgs[] = []; + (pub.watchContractEvent as jest.Mock).mockImplementation((args: WatchContractEventArgs) => { captured.push(args); return () => {}; }); + const handler = jest.fn(); + entity.events.watchDeadlineUpdated(handler); + const sig = keccak256(toHex("CampaignInfoDeadlineUpdated(uint256)")); + const data = ("0x" + "0".repeat(63) + "1") as `0x${string}`; + captured[0].onLogs([{ topics: [sig], data }]); + expect(handler).toHaveBeenCalled(); + }); + it("getLogs with fromBlock/toBlock options", async () => { + await entity.events.getDeadlineUpdatedLogs({ fromBlock: 0n, toBlock: 100n }); + expect(pub.getContractEvents).toHaveBeenCalledWith(expect.objectContaining({ fromBlock: 0n, toBlock: 100n })); + }); + }); }); // ============================================================ @@ -290,7 +465,53 @@ describe("PaymentTreasury entity", () => { it("cancelTreasury", async () => { await entity.simulate.cancelTreasury(B32); }); }); - it("events is empty", () => { expect(entity.events).toEqual({}); }); + describe("events", () => { + it("getPaymentCreatedLogs", async () => { await entity.events.getPaymentCreatedLogs(); expect(pub.getContractEvents).toHaveBeenCalled(); }); + it("getPaymentCancelledLogs", async () => { await entity.events.getPaymentCancelledLogs(); }); + it("getPaymentConfirmedLogs", async () => { await entity.events.getPaymentConfirmedLogs(); }); + it("getPaymentBatchConfirmedLogs", async () => { await entity.events.getPaymentBatchConfirmedLogs(); }); + it("getPaymentBatchCreatedLogs", async () => { await entity.events.getPaymentBatchCreatedLogs(); }); + it("getFeesDisbursedLogs", async () => { await entity.events.getFeesDisbursedLogs(); }); + it("getWithdrawalWithFeeSuccessfulLogs", async () => { await entity.events.getWithdrawalWithFeeSuccessfulLogs(); }); + it("getRefundClaimedLogs", async () => { await entity.events.getRefundClaimedLogs(); }); + it("getNonGoalLineItemsClaimedLogs", async () => { await entity.events.getNonGoalLineItemsClaimedLogs(); }); + it("getExpiredFundsClaimedLogs", async () => { await entity.events.getExpiredFundsClaimedLogs(); }); + it("watchPaymentCreated", () => { entity.events.watchPaymentCreated(() => {}); expect(pub.watchContractEvent).toHaveBeenCalled(); }); + it("watchPaymentConfirmed", () => { entity.events.watchPaymentConfirmed(() => {}); }); + it("watchPaymentCancelled", () => { entity.events.watchPaymentCancelled(() => {}); }); + it("watchPaymentBatchConfirmed", () => { entity.events.watchPaymentBatchConfirmed(() => {}); }); + it("watchPaymentBatchCreated", () => { entity.events.watchPaymentBatchCreated(() => {}); }); + it("watchRefundClaimed", () => { entity.events.watchRefundClaimed(() => {}); }); + it("watchFeesDisbursed", () => { entity.events.watchFeesDisbursed(() => {}); }); + it("watchWithdrawalWithFeeSuccessful", () => { entity.events.watchWithdrawalWithFeeSuccessful(() => {}); }); + it("watchNonGoalLineItemsClaimed", () => { entity.events.watchNonGoalLineItemsClaimed(() => {}); }); + it("watchExpiredFundsClaimed", () => { entity.events.watchExpiredFundsClaimed(() => {}); }); + it("decodeLog decodes a PaymentCancelled event", () => { + const sig = keccak256(toHex("PaymentCancelled(bytes32)")); + const result = entity.events.decodeLog({ topics: [sig, B32], data: "0x" as `0x${string}` }); + expect(result.eventName).toBe("PaymentCancelled"); + }); + it("fetchEventLogs decodes returned logs", async () => { + const sig = keccak256(toHex("PaymentCancelled(bytes32)")); + (pub.getContractEvents as jest.Mock).mockResolvedValueOnce([{ topics: [sig, B32], data: "0x" as `0x${string}` }]); + const logs = await entity.events.getPaymentCancelledLogs(); + expect(logs).toHaveLength(1); + expect(logs[0].eventName).toBe("PaymentCancelled"); + }); + it("watcher callback invokes handler", () => { + const captured: WatchContractEventArgs[] = []; + (pub.watchContractEvent as jest.Mock).mockImplementation((args: WatchContractEventArgs) => { captured.push(args); return () => {}; }); + const handler = jest.fn(); + entity.events.watchPaymentCreated(handler); + const sig = keccak256(toHex("PaymentCancelled(bytes32)")); + captured[0].onLogs([{ topics: [sig, B32], data: "0x" as `0x${string}` }]); + expect(handler).toHaveBeenCalled(); + }); + it("getLogs with fromBlock/toBlock options", async () => { + await entity.events.getPaymentCreatedLogs({ fromBlock: 0n, toBlock: 100n }); + expect(pub.getContractEvents).toHaveBeenCalledWith(expect.objectContaining({ fromBlock: 0n, toBlock: 100n })); + }); + }); }); // ============================================================ @@ -358,7 +579,57 @@ describe("AllOrNothing entity", () => { it("transferFrom", async () => { await entity.simulate.transferFrom(ADDR, ADDR, 0n); }); }); - it("events is empty", () => { expect(entity.events).toEqual({}); }); + describe("events", () => { + it("getReceiptLogs", async () => { await entity.events.getReceiptLogs(); expect(pub.getContractEvents).toHaveBeenCalled(); }); + it("getRefundClaimedLogs", async () => { await entity.events.getRefundClaimedLogs(); }); + it("getWithdrawalSuccessfulLogs", async () => { await entity.events.getWithdrawalSuccessfulLogs(); }); + it("getFeesDisbursedLogs", async () => { await entity.events.getFeesDisbursedLogs(); }); + it("getRewardsAddedLogs", async () => { await entity.events.getRewardsAddedLogs(); }); + it("getRewardRemovedLogs", async () => { await entity.events.getRewardRemovedLogs(); }); + it("getPausedLogs", async () => { await entity.events.getPausedLogs(); }); + it("getUnpausedLogs", async () => { await entity.events.getUnpausedLogs(); }); + it("getTransferLogs", async () => { await entity.events.getTransferLogs(); }); + it("getSuccessConditionNotFulfilledLogs", async () => { await entity.events.getSuccessConditionNotFulfilledLogs(); }); + it("getApprovalLogs", async () => { await entity.events.getApprovalLogs(); }); + it("getApprovalForAllLogs", async () => { await entity.events.getApprovalForAllLogs(); }); + it("watchReceipt", () => { entity.events.watchReceipt(() => {}); expect(pub.watchContractEvent).toHaveBeenCalled(); }); + it("watchRefundClaimed", () => { entity.events.watchRefundClaimed(() => {}); }); + it("watchWithdrawalSuccessful", () => { entity.events.watchWithdrawalSuccessful(() => {}); }); + it("watchFeesDisbursed", () => { entity.events.watchFeesDisbursed(() => {}); }); + it("watchRewardsAdded", () => { entity.events.watchRewardsAdded(() => {}); }); + it("watchRewardRemoved", () => { entity.events.watchRewardRemoved(() => {}); }); + it("watchPaused", () => { entity.events.watchPaused(() => {}); }); + it("watchUnpaused", () => { entity.events.watchUnpaused(() => {}); }); + it("watchTransfer", () => { entity.events.watchTransfer(() => {}); }); + it("watchSuccessConditionNotFulfilled", () => { entity.events.watchSuccessConditionNotFulfilled(() => {}); }); + it("watchApproval", () => { entity.events.watchApproval(() => {}); }); + it("watchApprovalForAll", () => { entity.events.watchApprovalForAll(() => {}); }); + it("decodeLog decodes a SuccessConditionNotFulfilled event", () => { + const sig = keccak256(toHex("SuccessConditionNotFulfilled()")); + const result = entity.events.decodeLog({ topics: [sig], data: "0x" as `0x${string}` }); + expect(result.eventName).toBe("SuccessConditionNotFulfilled"); + }); + it("fetchEventLogs decodes returned logs", async () => { + const sig = keccak256(toHex("SuccessConditionNotFulfilled()")); + (pub.getContractEvents as jest.Mock).mockResolvedValueOnce([{ topics: [sig], data: "0x" as `0x${string}` }]); + const logs = await entity.events.getSuccessConditionNotFulfilledLogs(); + expect(logs).toHaveLength(1); + expect(logs[0].eventName).toBe("SuccessConditionNotFulfilled"); + }); + it("watcher callback invokes handler", () => { + const captured: WatchContractEventArgs[] = []; + (pub.watchContractEvent as jest.Mock).mockImplementation((args: WatchContractEventArgs) => { captured.push(args); return () => {}; }); + const handler = jest.fn(); + entity.events.watchReceipt(handler); + const sig = keccak256(toHex("SuccessConditionNotFulfilled()")); + captured[0].onLogs([{ topics: [sig], data: "0x" as `0x${string}` }]); + expect(handler).toHaveBeenCalled(); + }); + it("getLogs with fromBlock/toBlock options", async () => { + await entity.events.getReceiptLogs({ fromBlock: 0n, toBlock: 100n }); + expect(pub.getContractEvents).toHaveBeenCalledWith(expect.objectContaining({ fromBlock: 0n, toBlock: 100n })); + }); + }); }); // ============================================================ @@ -461,7 +732,69 @@ describe("KeepWhatsRaised entity", () => { it("transferFrom", async () => { await entity.simulate.transferFrom(ADDR, ADDR, 0n); }); }); - it("events is empty", () => { expect(entity.events).toEqual({}); }); + describe("events", () => { + it("getReceiptLogs", async () => { await entity.events.getReceiptLogs(); expect(pub.getContractEvents).toHaveBeenCalled(); }); + it("getRefundClaimedLogs", async () => { await entity.events.getRefundClaimedLogs(); }); + it("getWithdrawalWithFeeSuccessfulLogs", async () => { await entity.events.getWithdrawalWithFeeSuccessfulLogs(); }); + it("getWithdrawalApprovedLogs", async () => { await entity.events.getWithdrawalApprovedLogs(); }); + it("getFeesDisbursedLogs", async () => { await entity.events.getFeesDisbursedLogs(); }); + it("getTreasuryConfiguredLogs", async () => { await entity.events.getTreasuryConfiguredLogs(); }); + it("getRewardsAddedLogs", async () => { await entity.events.getRewardsAddedLogs(); }); + it("getRewardRemovedLogs", async () => { await entity.events.getRewardRemovedLogs(); }); + it("getTipClaimedLogs", async () => { await entity.events.getTipClaimedLogs(); }); + it("getFundClaimedLogs", async () => { await entity.events.getFundClaimedLogs(); }); + it("getDeadlineUpdatedLogs", async () => { await entity.events.getDeadlineUpdatedLogs(); }); + it("getGoalAmountUpdatedLogs", async () => { await entity.events.getGoalAmountUpdatedLogs(); }); + it("getPaymentGatewayFeeSetLogs", async () => { await entity.events.getPaymentGatewayFeeSetLogs(); }); + it("getPausedLogs", async () => { await entity.events.getPausedLogs(); }); + it("getUnpausedLogs", async () => { await entity.events.getUnpausedLogs(); }); + it("getTransferLogs", async () => { await entity.events.getTransferLogs(); }); + it("getApprovalLogs", async () => { await entity.events.getApprovalLogs(); }); + it("getApprovalForAllLogs", async () => { await entity.events.getApprovalForAllLogs(); }); + it("watchReceipt", () => { entity.events.watchReceipt(() => {}); expect(pub.watchContractEvent).toHaveBeenCalled(); }); + it("watchRefundClaimed", () => { entity.events.watchRefundClaimed(() => {}); }); + it("watchWithdrawalWithFeeSuccessful", () => { entity.events.watchWithdrawalWithFeeSuccessful(() => {}); }); + it("watchWithdrawalApproved", () => { entity.events.watchWithdrawalApproved(() => {}); }); + it("watchFeesDisbursed", () => { entity.events.watchFeesDisbursed(() => {}); }); + it("watchTreasuryConfigured", () => { entity.events.watchTreasuryConfigured(() => {}); }); + it("watchRewardsAdded", () => { entity.events.watchRewardsAdded(() => {}); }); + it("watchRewardRemoved", () => { entity.events.watchRewardRemoved(() => {}); }); + it("watchTipClaimed", () => { entity.events.watchTipClaimed(() => {}); }); + it("watchFundClaimed", () => { entity.events.watchFundClaimed(() => {}); }); + it("watchDeadlineUpdated", () => { entity.events.watchDeadlineUpdated(() => {}); }); + it("watchGoalAmountUpdated", () => { entity.events.watchGoalAmountUpdated(() => {}); }); + it("watchPaymentGatewayFeeSet", () => { entity.events.watchPaymentGatewayFeeSet(() => {}); }); + it("watchPaused", () => { entity.events.watchPaused(() => {}); }); + it("watchUnpaused", () => { entity.events.watchUnpaused(() => {}); }); + it("watchTransfer", () => { entity.events.watchTransfer(() => {}); }); + it("watchApproval", () => { entity.events.watchApproval(() => {}); }); + it("watchApprovalForAll", () => { entity.events.watchApprovalForAll(() => {}); }); + it("decodeLog decodes a WithdrawalApproved event", () => { + const sig = keccak256(toHex("WithdrawalApproved()")); + const result = entity.events.decodeLog({ topics: [sig], data: "0x" as `0x${string}` }); + expect(result.eventName).toBe("WithdrawalApproved"); + }); + it("fetchEventLogs decodes returned logs", async () => { + const sig = keccak256(toHex("WithdrawalApproved()")); + (pub.getContractEvents as jest.Mock).mockResolvedValueOnce([{ topics: [sig], data: "0x" as `0x${string}` }]); + const logs = await entity.events.getWithdrawalApprovedLogs(); + expect(logs).toHaveLength(1); + expect(logs[0].eventName).toBe("WithdrawalApproved"); + }); + it("watcher callback invokes handler", () => { + const captured: WatchContractEventArgs[] = []; + (pub.watchContractEvent as jest.Mock).mockImplementation((args: WatchContractEventArgs) => { captured.push(args); return () => {}; }); + const handler = jest.fn(); + entity.events.watchReceipt(handler); + const sig = keccak256(toHex("WithdrawalApproved()")); + captured[0].onLogs([{ topics: [sig], data: "0x" as `0x${string}` }]); + expect(handler).toHaveBeenCalled(); + }); + it("getLogs with fromBlock/toBlock options", async () => { + await entity.events.getReceiptLogs({ fromBlock: 0n, toBlock: 100n }); + expect(pub.getContractEvents).toHaveBeenCalledWith(expect.objectContaining({ fromBlock: 0n, toBlock: 100n })); + }); + }); }); // ============================================================ @@ -480,7 +813,41 @@ describe("ItemRegistry entity", () => { it("addItemsBatch", async () => { await entity.addItemsBatch([B32], [item]); }); it("simulate.addItem", async () => { await entity.simulate.addItem(B32, item); }); it("simulate.addItemsBatch", async () => { await entity.simulate.addItemsBatch([B32], [item]); }); - it("events is empty", () => { expect(entity.events).toEqual({}); }); + describe("events", () => { + it("getItemAddedLogs", async () => { await entity.events.getItemAddedLogs(); expect(pub.getContractEvents).toHaveBeenCalled(); }); + it("watchItemAdded", () => { entity.events.watchItemAdded(() => {}); expect(pub.watchContractEvent).toHaveBeenCalled(); }); + it("decodeLog decodes an ItemAdded event", () => { + const sig = keccak256(toHex("ItemAdded(address,bytes32,(uint256,uint256,uint256,uint256,bytes32,bytes32))")); + const ownerTopic = ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}`; + const tupleData = ("0x" + "0".repeat(64).repeat(6)) as `0x${string}`; + const result = entity.events.decodeLog({ topics: [sig, ownerTopic, B32], data: tupleData }); + expect(result.eventName).toBe("ItemAdded"); + }); + it("fetchEventLogs decodes returned logs", async () => { + const sig = keccak256(toHex("ItemAdded(address,bytes32,(uint256,uint256,uint256,uint256,bytes32,bytes32))")); + const ownerTopic = ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}`; + const tupleData = ("0x" + "0".repeat(64).repeat(6)) as `0x${string}`; + (pub.getContractEvents as jest.Mock).mockResolvedValueOnce([{ topics: [sig, ownerTopic, B32], data: tupleData }]); + const logs = await entity.events.getItemAddedLogs(); + expect(logs).toHaveLength(1); + expect(logs[0].eventName).toBe("ItemAdded"); + }); + it("watcher callback invokes handler", () => { + const captured: WatchContractEventArgs[] = []; + (pub.watchContractEvent as jest.Mock).mockImplementation((args: WatchContractEventArgs) => { captured.push(args); return () => {}; }); + const handler = jest.fn(); + entity.events.watchItemAdded(handler); + const sig = keccak256(toHex("ItemAdded(address,bytes32,(uint256,uint256,uint256,uint256,bytes32,bytes32))")); + const ownerTopic = ("0x" + ADDR.slice(2).padStart(64, "0")) as `0x${string}`; + const tupleData = ("0x" + "0".repeat(64).repeat(6)) as `0x${string}`; + captured[0].onLogs([{ topics: [sig, ownerTopic, B32], data: tupleData }]); + expect(handler).toHaveBeenCalled(); + }); + it("getLogs with fromBlock/toBlock options", async () => { + await entity.events.getItemAddedLogs({ fromBlock: 0n, toBlock: 100n }); + expect(pub.getContractEvents).toHaveBeenCalledWith(expect.objectContaining({ fromBlock: 0n, toBlock: 100n })); + }); + }); }); // ============================================================ From 9560d19e0126e6546ab4724646549d8cf01c7ec8 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Wed, 1 Apr 2026 17:52:06 +0600 Subject: [PATCH 13/24] Add event handling documentation for contracts - Expanded the README.md to include detailed sections on event handling capabilities for various contracts, including methods for fetching historical logs, decoding raw logs, and watching live events. - Documented available events for GlobalParams, CampaignInfoFactory, TreasuryFactory, CampaignInfo, PaymentTreasury, AllOrNothing Treasury, and KeepWhatsRaised Treasury contracts, enhancing developer understanding of event interactions. --- packages/contracts/README.md | 267 +++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 650a2772..ab7b3a1a 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -461,6 +461,273 @@ await ir.addItemsBatch(itemIds, items); --- +## Events + +Every contract entity exposes an `events` property with three capabilities: + +1. **Fetch historical logs** — query past event logs from the blockchain +2. **Decode raw logs** — parse raw transaction receipt logs into typed event objects +3. **Watch live events** — subscribe to real-time event notifications + +### Fetching historical logs + +Each event has a `get*Logs()` method that returns all matching logs from the entire chain history. You can optionally pass `{ fromBlock, toBlock }` to narrow the search range. + +```typescript +const gp = oak.globalParams("0x..."); + +// All PlatformEnlisted events ever emitted by this contract +const logs = await gp.events.getPlatformEnlistedLogs(); + +for (const log of logs) { + console.log(log.eventName); // "PlatformEnlisted" + console.log(log.args); // { platformHash: "0x...", adminAddress: "0x...", ... } +} + +// Filter by block range +const recentLogs = await gp.events.getPlatformEnlistedLogs({ + fromBlock: 1_000_000n, + toBlock: 2_000_000n, +}); +``` + +### Decoding raw logs + +Use `decodeLog()` to decode a raw log from a transaction receipt. This is useful when you have a receipt and want to decode its logs without knowing which event they belong to. + +```typescript +const receipt = await oak.waitForReceipt(txHash); + +for (const log of receipt.logs) { + try { + const decoded = gp.events.decodeLog({ + topics: log.topics, + data: log.data, + }); + console.log(decoded.eventName, decoded.args); + } catch { + // Log doesn't match any event in this contract's ABI + } +} +``` + +### Watching live events + +Each event has a `watch*()` method that subscribes to real-time event notifications. The method returns an `unwatch` function to stop listening. + +```typescript +const gp = oak.globalParams("0x..."); + +// Start watching for new PlatformEnlisted events +const unwatch = gp.events.watchPlatformEnlisted((logs) => { + for (const log of logs) { + console.log("New platform enlisted:", log.args); + } +}); + +// Later — stop watching +unwatch(); +``` + +### Available events per contract + +#### GlobalParams + +```typescript +const gp = oak.globalParams("0x..."); + +// Fetch historical logs +await gp.events.getPlatformEnlistedLogs(options?); +await gp.events.getPlatformDelistedLogs(options?); +await gp.events.getPlatformAdminAddressUpdatedLogs(options?); +await gp.events.getPlatformDataAddedLogs(options?); +await gp.events.getPlatformDataRemovedLogs(options?); +await gp.events.getPlatformAdapterSetLogs(options?); +await gp.events.getPlatformClaimDelayUpdatedLogs(options?); +await gp.events.getProtocolAdminAddressUpdatedLogs(options?); +await gp.events.getProtocolFeePercentUpdatedLogs(options?); +await gp.events.getTokenAddedToCurrencyLogs(options?); +await gp.events.getTokenRemovedFromCurrencyLogs(options?); +await gp.events.getOwnershipTransferredLogs(options?); +await gp.events.getPausedLogs(options?); +await gp.events.getUnpausedLogs(options?); + +// Decode a raw log +gp.events.decodeLog({ topics, data }); + +// Watch live events +const unwatch = gp.events.watchPlatformEnlisted(handler); +const unwatch = gp.events.watchPlatformDelisted(handler); +const unwatch = gp.events.watchTokenAddedToCurrency(handler); +const unwatch = gp.events.watchTokenRemovedFromCurrency(handler); +``` + +#### CampaignInfoFactory + +```typescript +const factory = oak.campaignInfoFactory("0x..."); + +await factory.events.getCampaignCreatedLogs(options?); +await factory.events.getCampaignInitializedLogs(options?); +await factory.events.getOwnershipTransferredLogs(options?); +factory.events.decodeLog({ topics, data }); +const unwatch = factory.events.watchCampaignCreated(handler); +``` + +#### TreasuryFactory + +```typescript +const tf = oak.treasuryFactory("0x..."); + +await tf.events.getTreasuryDeployedLogs(options?); +await tf.events.getImplementationRegisteredLogs(options?); +await tf.events.getImplementationRemovedLogs(options?); +await tf.events.getImplementationApprovalLogs(options?); +tf.events.decodeLog({ topics, data }); +const unwatch = tf.events.watchTreasuryDeployed(handler); +const unwatch = tf.events.watchImplementationRegistered(handler); +``` + +#### CampaignInfo + +```typescript +const ci = oak.campaignInfo("0x..."); + +await ci.events.getDeadlineUpdatedLogs(options?); +await ci.events.getGoalAmountUpdatedLogs(options?); +await ci.events.getLaunchTimeUpdatedLogs(options?); +await ci.events.getPlatformInfoUpdatedLogs(options?); +await ci.events.getSelectedPlatformUpdatedLogs(options?); +await ci.events.getOwnershipTransferredLogs(options?); +await ci.events.getPausedLogs(options?); +await ci.events.getUnpausedLogs(options?); +ci.events.decodeLog({ topics, data }); +const unwatch = ci.events.watchDeadlineUpdated(handler); +const unwatch = ci.events.watchPlatformInfoUpdated(handler); +const unwatch = ci.events.watchSelectedPlatformUpdated(handler); +``` + +#### PaymentTreasury + +```typescript +const pt = oak.paymentTreasury("0x..."); + +await pt.events.getPaymentCreatedLogs(options?); +await pt.events.getPaymentCancelledLogs(options?); +await pt.events.getPaymentConfirmedLogs(options?); +await pt.events.getPaymentBatchConfirmedLogs(options?); +await pt.events.getPaymentBatchCreatedLogs(options?); +await pt.events.getFeesDisbursedLogs(options?); +await pt.events.getWithdrawalWithFeeSuccessfulLogs(options?); +await pt.events.getRefundClaimedLogs(options?); +await pt.events.getNonGoalLineItemsClaimedLogs(options?); +await pt.events.getExpiredFundsClaimedLogs(options?); +pt.events.decodeLog({ topics, data }); +const unwatch = pt.events.watchPaymentCreated(handler); +const unwatch = pt.events.watchPaymentConfirmed(handler); +const unwatch = pt.events.watchPaymentCancelled(handler); +const unwatch = pt.events.watchRefundClaimed(handler); +const unwatch = pt.events.watchFeesDisbursed(handler); +``` + +#### AllOrNothing Treasury + +```typescript +const aon = oak.allOrNothingTreasury("0x..."); + +await aon.events.getReceiptLogs(options?); +await aon.events.getRefundClaimedLogs(options?); +await aon.events.getWithdrawalSuccessfulLogs(options?); +await aon.events.getFeesDisbursedLogs(options?); +await aon.events.getRewardsAddedLogs(options?); +await aon.events.getRewardRemovedLogs(options?); +await aon.events.getPausedLogs(options?); +await aon.events.getUnpausedLogs(options?); +await aon.events.getTransferLogs(options?); +await aon.events.getSuccessConditionNotFulfilledLogs(options?); +aon.events.decodeLog({ topics, data }); +const unwatch = aon.events.watchReceipt(handler); +const unwatch = aon.events.watchRefundClaimed(handler); +const unwatch = aon.events.watchWithdrawalSuccessful(handler); +const unwatch = aon.events.watchFeesDisbursed(handler); +``` + +#### KeepWhatsRaised Treasury + +```typescript +const kwr = oak.keepWhatsRaisedTreasury("0x..."); + +await kwr.events.getReceiptLogs(options?); +await kwr.events.getRefundClaimedLogs(options?); +await kwr.events.getWithdrawalWithFeeSuccessfulLogs(options?); +await kwr.events.getWithdrawalApprovedLogs(options?); +await kwr.events.getFeesDisbursedLogs(options?); +await kwr.events.getTreasuryConfiguredLogs(options?); +await kwr.events.getRewardsAddedLogs(options?); +await kwr.events.getRewardRemovedLogs(options?); +await kwr.events.getTipClaimedLogs(options?); +await kwr.events.getFundClaimedLogs(options?); +await kwr.events.getDeadlineUpdatedLogs(options?); +await kwr.events.getGoalAmountUpdatedLogs(options?); +await kwr.events.getPaymentGatewayFeeSetLogs(options?); +await kwr.events.getPausedLogs(options?); +await kwr.events.getUnpausedLogs(options?); +await kwr.events.getTransferLogs(options?); +kwr.events.decodeLog({ topics, data }); +const unwatch = kwr.events.watchReceipt(handler); +const unwatch = kwr.events.watchRefundClaimed(handler); +const unwatch = kwr.events.watchWithdrawalWithFeeSuccessful(handler); +const unwatch = kwr.events.watchFeesDisbursed(handler); +``` + +#### ItemRegistry + +```typescript +const ir = oak.itemRegistry("0x..."); + +await ir.events.getItemAddedLogs(options?); +ir.events.decodeLog({ topics, data }); +const unwatch = ir.events.watchItemAdded(handler); +``` + +### Types + +All event methods use shared types from `@oaknetwork/contracts`: + +```typescript +import type { + DecodedEventLog, + EventFilterOptions, + EventWatchHandler, + RawLog, +} from "@oaknetwork/contracts"; + +// EventFilterOptions — optional block range for get*Logs +interface EventFilterOptions { + fromBlock?: bigint; // defaults to 0n (genesis) if omitted + toBlock?: bigint; // defaults to latest block if omitted +} + +// DecodedEventLog — returned by get*Logs and decodeLog +interface DecodedEventLog { + eventName: string; + args: Record; +} + +// RawLog — input to decodeLog +interface RawLog { + topics: readonly `0x${string}`[]; + data: `0x${string}`; +} + +// EventWatchHandler — callback for watch* methods +type EventWatchHandler = (logs: readonly DecodedEventLog[]) => void; +``` + +> For complete details on contract events, please visit the following link: [Events](https://oaknetwork.org/docs/contracts-sdk/events). + +--- + ## Error Handling Contract calls can revert with on-chain errors. The SDK decodes raw revert data into typed error classes with decoded arguments and human-readable recovery hints. From 62b7ea9f5b5875fe6694878974f6b9a5e2e84d22 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Thu, 2 Apr 2026 14:41:27 +0600 Subject: [PATCH 14/24] Add multicall utility for batching read calls - Introduced a new `multicall` function to enable batching of multiple entity read calls into a single RPC request, improving efficiency. - Updated `provider.ts` to enable `batch.multicall` in the JSON RPC provider configuration. - Exported the new `multicall` function from the utils index for easy access. --- packages/contracts/src/lib/viem/provider.ts | 1 + packages/contracts/src/utils/index.ts | 1 + packages/contracts/src/utils/multicall.ts | 33 +++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 packages/contracts/src/utils/multicall.ts diff --git a/packages/contracts/src/lib/viem/provider.ts b/packages/contracts/src/lib/viem/provider.ts index dda49a1d..c03cbedd 100644 --- a/packages/contracts/src/lib/viem/provider.ts +++ b/packages/contracts/src/lib/viem/provider.ts @@ -32,6 +32,7 @@ export function createJsonRpcProvider( return createPublicClient({ chain, transport: http(rpcUrl, { timeout }), + batch: { multicall: true }, }) as JsonRpcProvider; } diff --git a/packages/contracts/src/utils/index.ts b/packages/contracts/src/utils/index.ts index 4ff7115b..c6c69b1b 100644 --- a/packages/contracts/src/utils/index.ts +++ b/packages/contracts/src/utils/index.ts @@ -7,3 +7,4 @@ export { isHex, toHex } from "./hex"; export { keccak256, id } from "./hash"; export { getCurrentTimestamp, addDays } from "./time"; export { getChainFromId } from "./chain"; +export { multicall } from "./multicall"; diff --git a/packages/contracts/src/utils/multicall.ts b/packages/contracts/src/utils/multicall.ts new file mode 100644 index 00000000..144d98f8 --- /dev/null +++ b/packages/contracts/src/utils/multicall.ts @@ -0,0 +1,33 @@ +/** + * @file utils/multicall.ts + * Ergonomic multicall helper that batches multiple entity read calls into a + * single RPC round-trip. Works by running closures concurrently via + * {@link Promise.all}; viem's `batch.multicall` transport option (enabled by + * default in the SDK) automatically aggregates all `readContract` calls + * dispatched within the same tick into one Multicall3 on-chain call. + * + * @example + * ```typescript + * const gp = oak.globalParams(address); + * const [count, fee] = await multicall([ + * () => gp.getNumberOfListedPlatforms(), + * () => gp.getProtocolFeePercent(), + * ]); + * ``` + */ + +/** + * Executes an array of lazy read calls concurrently. When the underlying + * `PublicClient` has `batch.multicall` enabled (SDK default), all + * `readContract` invocations within the same tick are automatically + * aggregated into a single Multicall3 RPC request. + * + * @param calls - Array of zero-argument functions that each return a Promise + * @returns Array of resolved values in the same order as the input calls + */ +export async function multicall Promise)[]>( + calls: [...T], +): Promise<{ [K in keyof T]: Awaited> }> { + const results = await Promise.all(calls.map((fn) => fn())); + return results as { [K in keyof T]: Awaited> }; +} From ec0376bdc0e3ba81b982a2b8946f17c4f8dc3d8c Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Thu, 2 Apr 2026 14:42:47 +0600 Subject: [PATCH 15/24] Add multicall method to OakContractsClient interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented a new `multicall` method in the `OakContractsClient` interface to facilitate batching of multiple entity read calls into a single RPC request. - Updated the `create.ts` file to include the `multicall` function, enhancing the client’s functionality and efficiency in handling multiple asynchronous calls. --- packages/contracts/src/client/create.ts | 7 +++++++ packages/contracts/src/client/types.ts | 21 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/client/create.ts b/packages/contracts/src/client/create.ts index 1e956d32..2ac3232b 100644 --- a/packages/contracts/src/client/create.ts +++ b/packages/contracts/src/client/create.ts @@ -14,6 +14,7 @@ import type { ItemRegistryEntity, } from "./types"; import { DEFAULT_CLIENT_OPTIONS, type OakContractsClientOptions, type EntitySignerOptions } from "./types"; +import { multicall } from "../utils/multicall"; import { buildClients } from "./resolve"; import { createGlobalParamsEntity } from "../contracts/global-params"; import { createCampaignInfoFactoryEntity } from "../contracts/campaign-info-factory"; @@ -64,6 +65,12 @@ export function createOakContractsClient( walletClient, waitForReceipt, + multicall Promise)[]>( + calls: [...T], + ): Promise<{ [K in keyof T]: Awaited> }> { + return multicall(calls); + }, + globalParams(address: Address, options?: EntitySignerOptions): GlobalParamsEntity { return createGlobalParamsEntity(address, publicClient, options?.signer ?? walletClient, chain); }, diff --git a/packages/contracts/src/client/types.ts b/packages/contracts/src/client/types.ts index b818d003..6065b759 100644 --- a/packages/contracts/src/client/types.ts +++ b/packages/contracts/src/client/types.ts @@ -113,7 +113,6 @@ export type { ItemRegistryEntity, }; - /** Oak Contracts SDK client; entity factories and receipt helper. */ export interface OakContractsClient { /** Public chain configuration (no secrets). */ @@ -130,6 +129,26 @@ export interface OakContractsClient { * @returns TransactionReceipt with blockNumber, gasUsed, and logs */ waitForReceipt(txHash: Hex): Promise; + /** + * Batches multiple entity read calls into a single RPC round-trip via the + * on-chain Multicall3 contract. Accepts an array of lazy read closures — + * the same calls you would normally `await` individually. + * + * @param calls - Array of zero-argument functions that each return a Promise + * @returns Tuple of resolved values in the same order as the input calls + * + * @example + * ```typescript + * const gp = oak.globalParams(address); + * const [count, fee] = await oak.multicall([ + * () => gp.getNumberOfListedPlatforms(), + * () => gp.getProtocolFeePercent(), + * ]); + * ``` + */ + multicall Promise)[]>( + calls: [...T], + ): Promise<{ [K in keyof T]: Awaited> }>; /** Returns a GlobalParams entity for the given contract address. */ globalParams(address: Address, options?: EntitySignerOptions): GlobalParamsEntity; /** Returns a CampaignInfoFactory entity for the given contract address. */ From 08a1bcce630fb99fef4436887d2b7aec8efd5bfc Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Thu, 2 Apr 2026 15:06:28 +0600 Subject: [PATCH 16/24] Implement campaign and treasury financial aggregation methods - Added `getCampaignSummary` function to aggregate financial data from the CampaignInfo contract, including total raised, refunded, and goal status. - Introduced `getTreasuryReport` function for per-treasury reporting, supporting AllOrNothing, KeepWhatsRaised, and PaymentTreasury contracts. - Updated types to reflect new options and return structures for campaign and treasury reports. - Enhanced documentation with examples for both functions to improve usability. --- packages/contracts/src/metrics/campaign.ts | 67 +++++++++++++++--- packages/contracts/src/metrics/index.ts | 17 ++++- packages/contracts/src/metrics/platform.ts | 40 +++++++++-- packages/contracts/src/metrics/treasury.ts | 69 ++++++++++++++++--- packages/contracts/src/metrics/types.ts | 80 +++++++++++++++++----- packages/contracts/src/types/index.ts | 4 +- 6 files changed, 234 insertions(+), 43 deletions(-) diff --git a/packages/contracts/src/metrics/campaign.ts b/packages/contracts/src/metrics/campaign.ts index 04f94a98..2fe7ac3a 100644 --- a/packages/contracts/src/metrics/campaign.ts +++ b/packages/contracts/src/metrics/campaign.ts @@ -1,18 +1,69 @@ /** * @file metrics/campaign.ts - * TODO: Implement campaign-level aggregation across treasuries. + * Campaign-level financial aggregation from CampaignInfo. + * Reads are dispatched concurrently; viem's `batch.multicall` transport + * automatically aggregates them into a single Multicall3 RPC round-trip. */ -import type { CampaignSummary } from "./types"; +import { CAMPAIGN_INFO_ABI } from "../contracts/campaign-info/abi"; +import type { CampaignSummary, CampaignSummaryOptions } from "./types"; /** - * Aggregates state from all treasuries linked to a campaign. - * @param _campaignInfoAddress - Deployed CampaignInfo contract address - * @returns CampaignSummary — currently a stub returning empty summary + * Aggregates financial state from a deployed CampaignInfo contract. + * + * CampaignInfo already maintains running totals across all linked treasuries, + * so this function reads those aggregated values directly rather than iterating + * individual treasury contracts. + * + * @param options - CampaignInfo address and PublicClient for on-chain reads + * @returns CampaignSummary with raised, refunded, cancelled, expected amounts and goal status + * + * @example + * ```typescript + * const summary = await getCampaignSummary({ + * campaignInfoAddress: "0x...", + * publicClient, + * }); + * if (summary.goalReached) { + * console.log("Campaign goal met!"); + * } + * ``` */ export async function getCampaignSummary( - _campaignInfoAddress: string, + options: CampaignSummaryOptions, ): Promise { - // TODO: implement by reading linked treasury contracts via CampaignInfo - return {}; + const { campaignInfoAddress, publicClient } = options; + const contract = { address: campaignInfoAddress, abi: CAMPAIGN_INFO_ABI } as const; + + const [ + totalRaised, + totalLifetimeRaised, + totalRefunded, + totalAvailable, + totalCancelled, + totalExpected, + goalAmount, + ] = await Promise.all([ + publicClient.readContract({ ...contract, functionName: "getTotalRaisedAmount" }), + publicClient.readContract({ ...contract, functionName: "getTotalLifetimeRaisedAmount" }), + publicClient.readContract({ ...contract, functionName: "getTotalRefundedAmount" }), + publicClient.readContract({ ...contract, functionName: "getTotalAvailableRaisedAmount" }), + publicClient.readContract({ ...contract, functionName: "getTotalCancelledAmount" }), + publicClient.readContract({ ...contract, functionName: "getTotalExpectedAmount" }), + publicClient.readContract({ ...contract, functionName: "getGoalAmount" }), + ]); + + const raised = totalRaised as bigint; + const goal = goalAmount as bigint; + + return { + totalRaised: raised, + totalLifetimeRaised: totalLifetimeRaised as bigint, + totalRefunded: totalRefunded as bigint, + totalAvailable: totalAvailable as bigint, + totalCancelled: totalCancelled as bigint, + totalExpected: totalExpected as bigint, + goalAmount: goal, + goalReached: raised >= goal, + }; } diff --git a/packages/contracts/src/metrics/index.ts b/packages/contracts/src/metrics/index.ts index 97782c34..14f9fe37 100644 --- a/packages/contracts/src/metrics/index.ts +++ b/packages/contracts/src/metrics/index.ts @@ -1,10 +1,21 @@ /** * @file metrics/index.ts - * Public surface for the @oaknetwork/contracts/metrics sub-path export. - * TODO: Register in package.json exports as `@oaknetwork/contracts/metrics`. + * Public surface for the `@oaknetwork/contracts/metrics` sub-path export. + * + * - platform.ts — protocol-level statistics from GlobalParams. + * - campaign.ts — campaign-level financial aggregation from CampaignInfo. + * - treasury.ts — per-treasury reporting for AllOrNothing, KeepWhatsRaised, and PaymentTreasury. */ export { getPlatformStats } from "./platform"; export { getCampaignSummary } from "./campaign"; export { getTreasuryReport } from "./treasury"; -export type { PlatformStats, CampaignSummary, TreasuryReport } from "./types"; +export type { + PlatformStats, + PlatformStatsOptions, + CampaignSummary, + CampaignSummaryOptions, + TreasuryReport, + TreasuryReportOptions, + TreasuryType, +} from "./types"; diff --git a/packages/contracts/src/metrics/platform.ts b/packages/contracts/src/metrics/platform.ts index ecc7da61..6ff86439 100644 --- a/packages/contracts/src/metrics/platform.ts +++ b/packages/contracts/src/metrics/platform.ts @@ -1,15 +1,41 @@ /** * @file metrics/platform.ts - * TODO: Implement with multicall where supported. + * Protocol-level statistics aggregated from GlobalParams. + * Reads are dispatched concurrently; viem's `batch.multicall` transport + * automatically aggregates them into a single Multicall3 RPC round-trip. */ -import type { PlatformStats } from "./types"; +import { GLOBAL_PARAMS_ABI } from "../contracts/global-params/abi"; +import type { PlatformStats, PlatformStatsOptions } from "./types"; /** - * Aggregates protocol-level statistics from GlobalParams and all treasury contracts. - * @returns PlatformStats — currently a stub returning empty stats + * Aggregates protocol-level statistics from a deployed GlobalParams contract. + * + * @param options - GlobalParams address and PublicClient for on-chain reads + * @returns PlatformStats with platform count and protocol fee percent + * + * @example + * ```typescript + * const stats = await getPlatformStats({ + * globalParamsAddress: "0x...", + * publicClient, + * }); + * console.log(`${stats.platformCount} platforms enlisted`); + * ``` */ -export async function getPlatformStats(): Promise { - // TODO: implement using multicall across GlobalParams and treasury contracts - return {}; +export async function getPlatformStats( + options: PlatformStatsOptions, +): Promise { + const { globalParamsAddress, publicClient } = options; + const contract = { address: globalParamsAddress, abi: GLOBAL_PARAMS_ABI } as const; + + const [platformCount, protocolFeePercent] = await Promise.all([ + publicClient.readContract({ ...contract, functionName: "getNumberOfListedPlatforms" }), + publicClient.readContract({ ...contract, functionName: "getProtocolFeePercent" }), + ]); + + return { + platformCount: platformCount as bigint, + protocolFeePercent: protocolFeePercent as bigint, + }; } diff --git a/packages/contracts/src/metrics/treasury.ts b/packages/contracts/src/metrics/treasury.ts index 1b190798..a64f0b5e 100644 --- a/packages/contracts/src/metrics/treasury.ts +++ b/packages/contracts/src/metrics/treasury.ts @@ -1,18 +1,71 @@ /** * @file metrics/treasury.ts - * TODO: Implement per-treasury reporting. + * Per-treasury reporting for AllOrNothing, KeepWhatsRaised, and PaymentTreasury. + * Reads are dispatched concurrently; viem's `batch.multicall` transport + * automatically aggregates them into a single Multicall3 RPC round-trip. */ -import type { TreasuryReport } from "./types"; +import { ALL_OR_NOTHING_ABI } from "../contracts/all-or-nothing/abi"; +import { KEEP_WHATS_RAISED_ABI } from "../contracts/keep-whats-raised/abi"; +import { PAYMENT_TREASURY_ABI } from "../contracts/payment-treasury/abi"; +import type { TreasuryReport, TreasuryReportOptions, TreasuryType } from "./types"; + +const ABI_BY_TYPE: Record = { + "all-or-nothing": ALL_OR_NOTHING_ABI, + "keep-whats-raised": KEEP_WHATS_RAISED_ABI, + "payment-treasury": PAYMENT_TREASURY_ABI, +}; + +/** + * The on-chain function name for `getPlatformFeePercent` differs between + * treasury contracts. PaymentTreasury uses a lowercase-p ABI name. + */ +const FEE_FN_BY_TYPE: Record = { + "all-or-nothing": "getPlatformFeePercent", + "keep-whats-raised": "getPlatformFeePercent", + "payment-treasury": "getplatformFeePercent", +}; /** - * Builds a report for a single treasury contract address. - * @param _treasuryAddress - Deployed treasury contract address - * @returns TreasuryReport — currently a stub returning empty report + * Builds a financial report for a single deployed treasury contract. + * + * @param options - Treasury address, type discriminator, and PublicClient + * @returns TreasuryReport with raised/refunded amounts, fee percent, and cancellation status + * + * @example + * ```typescript + * const report = await getTreasuryReport({ + * treasuryAddress: "0x...", + * treasuryType: "all-or-nothing", + * publicClient, + * }); + * console.log(`Raised: ${report.raisedAmount}`); + * ``` */ export async function getTreasuryReport( - _treasuryAddress: string, + options: TreasuryReportOptions, ): Promise { - // TODO: implement by reading raised/refunded amounts and fee config - return {}; + const { treasuryAddress, treasuryType, publicClient } = options; + const abi = ABI_BY_TYPE[treasuryType]; + const feeFn = FEE_FN_BY_TYPE[treasuryType]; + const contract = { address: treasuryAddress, abi } as const; + + const [raisedAmount, lifetimeRaisedAmount, refundedAmount, platformFeePercent, cancelled] = + await Promise.all([ + publicClient.readContract({ ...contract, functionName: "getRaisedAmount" }), + publicClient.readContract({ ...contract, functionName: "getLifetimeRaisedAmount" }), + publicClient.readContract({ ...contract, functionName: "getRefundedAmount" }), + publicClient.readContract({ ...contract, functionName: feeFn }), + publicClient.readContract({ ...contract, functionName: "cancelled" }), + ]); + + return { + address: treasuryAddress, + treasuryType, + raisedAmount: raisedAmount as bigint, + lifetimeRaisedAmount: lifetimeRaisedAmount as bigint, + refundedAmount: refundedAmount as bigint, + platformFeePercent: platformFeePercent as bigint, + cancelled: cancelled as boolean, + }; } diff --git a/packages/contracts/src/metrics/types.ts b/packages/contracts/src/metrics/types.ts index 891adb0a..3bafa6f1 100644 --- a/packages/contracts/src/metrics/types.ts +++ b/packages/contracts/src/metrics/types.ts @@ -1,33 +1,81 @@ /** * @file metrics/types.ts - * Placeholder types for cross-contract aggregation results. - * TODO: Complete shapes once multicall-based aggregation is implemented. + * Types for cross-contract aggregation results returned by the metrics module. */ -/** Aggregated protocol-level statistics across all campaigns. */ +import type { Address, PublicClient } from "../lib"; + +/** Options for {@link getPlatformStats}. */ +export interface PlatformStatsOptions { + /** Deployed GlobalParams contract address. */ + globalParamsAddress: Address; + /** Viem PublicClient for on-chain reads. */ + publicClient: PublicClient; +} + +/** Aggregated protocol-level statistics from GlobalParams. */ export interface PlatformStats { /** Total number of enlisted platforms. */ - platformCount?: bigint; - /** Total protocol fees collected across all treasuries. */ - totalProtocolFees?: bigint; + platformCount: bigint; + /** Protocol fee percent in basis points. */ + protocolFeePercent: bigint; +} + +/** Options for {@link getCampaignSummary}. */ +export interface CampaignSummaryOptions { + /** Deployed CampaignInfo contract address. */ + campaignInfoAddress: Address; + /** Viem PublicClient for on-chain reads. */ + publicClient: PublicClient; } -/** Summary of a single campaign's treasury state. */ +/** Summary of a single campaign's financial state. */ export interface CampaignSummary { - /** Total amount raised across all treasury types. */ - totalRaised?: bigint; - /** Total amount refunded. */ - totalRefunded?: bigint; - /** Whether the campaign goal has been reached. */ - goalReached?: boolean; + /** Total amount raised across all linked treasuries. */ + totalRaised: bigint; + /** Lifetime raised amount (includes refunded funds). */ + totalLifetimeRaised: bigint; + /** Total amount refunded to backers. */ + totalRefunded: bigint; + /** Available raised amount after refunds and fees. */ + totalAvailable: bigint; + /** Total cancelled payment amount. */ + totalCancelled: bigint; + /** Total expected payment amount. */ + totalExpected: bigint; + /** Campaign funding goal. */ + goalAmount: bigint; + /** Whether the total raised meets or exceeds the goal. */ + goalReached: boolean; +} + +/** Treasury type discriminator for {@link TreasuryReport}. */ +export type TreasuryType = "all-or-nothing" | "keep-whats-raised" | "payment-treasury"; + +/** Options for {@link getTreasuryReport}. */ +export interface TreasuryReportOptions { + /** Deployed treasury contract address. */ + treasuryAddress: Address; + /** Which type of treasury contract is at the address. */ + treasuryType: TreasuryType; + /** Viem PublicClient for on-chain reads. */ + publicClient: PublicClient; } /** Aggregated report for a single treasury contract. */ export interface TreasuryReport { /** Address of the treasury contract. */ - address?: string; + address: Address; + /** Which treasury type this report was built from. */ + treasuryType: TreasuryType; /** Current raised amount held in the treasury. */ - raisedAmount?: bigint; + raisedAmount: bigint; + /** Lifetime raised amount including refunded funds. */ + lifetimeRaisedAmount: bigint; + /** Total amount refunded. */ + refundedAmount: bigint; /** Platform fee percent in basis points. */ - platformFeePercent?: bigint; + platformFeePercent: bigint; + /** Whether the treasury has been cancelled. */ + cancelled: boolean; } diff --git a/packages/contracts/src/types/index.ts b/packages/contracts/src/types/index.ts index 39de5567..403d3c7b 100644 --- a/packages/contracts/src/types/index.ts +++ b/packages/contracts/src/types/index.ts @@ -1,6 +1,8 @@ /** * Cross-contract type definitions only — no logic, no client dependencies. - * structs.ts holds on-chain struct mirrors; params.ts holds SDK-level input types. + * + * - structs.ts — on-chain struct mirrors (e.g. reward tuples, item tuples). + * - params.ts — SDK-level input types used by write/simulate methods. */ export * from "./structs"; export * from "./params"; From 278fa14a14d20af68770ac219048bfcc1196a4b6 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Thu, 2 Apr 2026 15:06:49 +0600 Subject: [PATCH 17/24] Add tests for multicall utility and enhance metrics functions - Introduced tests for the `multicall` utility, verifying concurrent execution of closures and error propagation. - Expanded the `getPlatformStats`, `getCampaignSummary`, and `getTreasuryReport` functions with comprehensive tests to ensure accurate financial data aggregation and reporting. - Implemented mock client for testing contract interactions, improving test coverage and reliability. --- .../contracts/__tests__/unit/client.test.ts | 15 ++ .../contracts/__tests__/unit/metrics.test.ts | 206 +++++++++++++++++- 2 files changed, 211 insertions(+), 10 deletions(-) diff --git a/packages/contracts/__tests__/unit/client.test.ts b/packages/contracts/__tests__/unit/client.test.ts index f9d9f245..8f6b5944 100644 --- a/packages/contracts/__tests__/unit/client.test.ts +++ b/packages/contracts/__tests__/unit/client.test.ts @@ -122,6 +122,21 @@ describe("createOakContractsClient", () => { expect(typeof client.waitForReceipt).toBe("function"); }); + it("multicall runs closures concurrently and returns results", async () => { + const client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: RPC, + privateKey: PK, + }); + + const results = await client.multicall([ + () => Promise.resolve(5n), + () => Promise.resolve(250n), + ]); + + expect(results).toEqual([5n, 250n]); + }); + it("waitForReceipt calls publicClient.waitForTransactionReceipt", async () => { const client = createOakContractsClient({ chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, diff --git a/packages/contracts/__tests__/unit/metrics.test.ts b/packages/contracts/__tests__/unit/metrics.test.ts index 3f7f97e6..dd1d6055 100644 --- a/packages/contracts/__tests__/unit/metrics.test.ts +++ b/packages/contracts/__tests__/unit/metrics.test.ts @@ -1,20 +1,206 @@ +import type { Address, PublicClient } from "../../src/lib"; import { getPlatformStats } from "../../src/metrics/platform"; import { getCampaignSummary } from "../../src/metrics/campaign"; import { getTreasuryReport } from "../../src/metrics/treasury"; +import { multicall } from "../../src/utils/multicall"; +import type { TreasuryType } from "../../src/metrics/types"; -describe("metrics stubs", () => { - it("getPlatformStats returns empty object", async () => { - const result = await getPlatformStats(); - expect(result).toEqual({}); +const ADDR = "0x0000000000000000000000000000000000000001" as Address; + +/** + * Builds a mock PublicClient whose `.readContract` resolves per-functionName. + */ +function mockPublicClient(returnValues: Record = {}): PublicClient { + return { + readContract: jest.fn().mockImplementation( + ({ functionName }: { functionName: string }) => + Promise.resolve( + functionName in returnValues ? returnValues[functionName] : 0n, + ), + ), + } as unknown as PublicClient; +} + +// ────────────────────────────────────────────────────────────────────────────── +// multicall utility +// ────────────────────────────────────────────────────────────────────────────── + +describe("multicall utility", () => { + it("runs all closures concurrently and returns results in order", async () => { + const results = await multicall([ + () => Promise.resolve(5n), + () => Promise.resolve("hello"), + () => Promise.resolve(true), + ]); + + expect(results).toEqual([5n, "hello", true]); + }); + + it("returns an empty array for empty input", async () => { + const results = await multicall([]); + expect(results).toEqual([]); + }); + + it("propagates errors from failed closures", async () => { + await expect( + multicall([ + () => Promise.resolve(1n), + () => Promise.reject(new Error("boom")), + ]), + ).rejects.toThrow("boom"); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// getPlatformStats +// ────────────────────────────────────────────────────────────────────────────── + +describe("getPlatformStats", () => { + it("returns platform count and protocol fee percent from GlobalParams", async () => { + const pub = mockPublicClient({ + getNumberOfListedPlatforms: 5n, + getProtocolFeePercent: 250n, + }); + + const stats = await getPlatformStats({ + globalParamsAddress: ADDR, + publicClient: pub, + }); + + expect(stats.platformCount).toBe(5n); + expect(stats.protocolFeePercent).toBe(250n); + expect(pub.readContract).toHaveBeenCalledTimes(2); }); - it("getCampaignSummary returns empty object", async () => { - const result = await getCampaignSummary("0x1234567890abcdef1234567890abcdef12345678"); - expect(result).toEqual({}); + it("returns zero values when contract returns defaults", async () => { + const pub = mockPublicClient(); + + const stats = await getPlatformStats({ + globalParamsAddress: ADDR, + publicClient: pub, + }); + + expect(stats.platformCount).toBe(0n); + expect(stats.protocolFeePercent).toBe(0n); }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// getCampaignSummary +// ────────────────────────────────────────────────────────────────────────────── + +describe("getCampaignSummary", () => { + it("returns all aggregated campaign values", async () => { + const pub = mockPublicClient({ + getTotalRaisedAmount: 1000n, + getTotalLifetimeRaisedAmount: 1200n, + getTotalRefundedAmount: 200n, + getTotalAvailableRaisedAmount: 800n, + getTotalCancelledAmount: 50n, + getTotalExpectedAmount: 300n, + getGoalAmount: 500n, + }); + + const summary = await getCampaignSummary({ + campaignInfoAddress: ADDR, + publicClient: pub, + }); + + expect(summary.totalRaised).toBe(1000n); + expect(summary.totalLifetimeRaised).toBe(1200n); + expect(summary.totalRefunded).toBe(200n); + expect(summary.totalAvailable).toBe(800n); + expect(summary.totalCancelled).toBe(50n); + expect(summary.totalExpected).toBe(300n); + expect(summary.goalAmount).toBe(500n); + expect(summary.goalReached).toBe(true); + expect(pub.readContract).toHaveBeenCalledTimes(7); + }); + + it("sets goalReached to false when raised < goal", async () => { + const pub = mockPublicClient({ + getTotalRaisedAmount: 100n, + getGoalAmount: 500n, + }); + + const summary = await getCampaignSummary({ + campaignInfoAddress: ADDR, + publicClient: pub, + }); + + expect(summary.goalReached).toBe(false); + }); + + it("sets goalReached to true when raised equals goal exactly", async () => { + const pub = mockPublicClient({ + getTotalRaisedAmount: 500n, + getGoalAmount: 500n, + }); + + const summary = await getCampaignSummary({ + campaignInfoAddress: ADDR, + publicClient: pub, + }); + + expect(summary.goalReached).toBe(true); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// getTreasuryReport +// ────────────────────────────────────────────────────────────────────────────── + +describe("getTreasuryReport", () => { + const treasuryTypes: TreasuryType[] = [ + "all-or-nothing", + "keep-whats-raised", + "payment-treasury", + ]; + + it.each(treasuryTypes)( + "returns correct report for %s treasury", + async (treasuryType) => { + const feeFnName = + treasuryType === "payment-treasury" + ? "getplatformFeePercent" + : "getPlatformFeePercent"; + + const pub = mockPublicClient({ + getRaisedAmount: 5000n, + getLifetimeRaisedAmount: 7000n, + getRefundedAmount: 2000n, + [feeFnName]: 300n, + cancelled: false, + }); + + const report = await getTreasuryReport({ + treasuryAddress: ADDR, + treasuryType, + publicClient: pub, + }); + + expect(report.address).toBe(ADDR); + expect(report.treasuryType).toBe(treasuryType); + expect(report.raisedAmount).toBe(5000n); + expect(report.lifetimeRaisedAmount).toBe(7000n); + expect(report.refundedAmount).toBe(2000n); + expect(report.platformFeePercent).toBe(300n); + expect(report.cancelled).toBe(false); + expect(pub.readContract).toHaveBeenCalledTimes(5); + }, + ); + + it("reports cancelled treasury", async () => { + const pub = mockPublicClient({ + cancelled: true, + }); + + const report = await getTreasuryReport({ + treasuryAddress: ADDR, + treasuryType: "all-or-nothing", + publicClient: pub, + }); - it("getTreasuryReport returns empty object", async () => { - const result = await getTreasuryReport("0x1234567890abcdef1234567890abcdef12345678"); - expect(result).toEqual({}); + expect(report.cancelled).toBe(true); }); }); From 7a25636fb97f6c4ef541c580bbfdf3e6ed58104f Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Thu, 2 Apr 2026 15:07:39 +0600 Subject: [PATCH 18/24] Add Multicall and Metrics Documentation to README - Introduced a new section on Multicall, detailing its usage for batching multiple entity read calls into a single RPC request, with examples for standalone utility and client convenience methods. - Added a Metrics section, outlining pre-built aggregation functions for platform statistics, campaign summaries, and treasury reports, complete with usage examples. - Updated the exported entry points to include the new multicall and metrics functionalities for improved accessibility. --- packages/contracts/README.md | 130 +++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 7 deletions(-) diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 650a2772..53679c1c 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -461,6 +461,118 @@ await ir.addItemsBatch(itemIds, items); --- +## Multicall + +Batch multiple entity read calls into a single RPC round-trip via the on-chain Multicall3 contract. Pass an array of lazy closures — the same entity read methods you'd normally `await` individually. + +### Standalone utility + +```typescript +import { multicall } from "@oaknetwork/contracts"; + +const gp = oak.globalParams("0x..."); + +const [platformCount, feePercent, admin] = await multicall([ + () => gp.getNumberOfListedPlatforms(), + () => gp.getProtocolFeePercent(), + () => gp.getProtocolAdminAddress(), +]); +``` + +### Client convenience method + +```typescript +const gp = oak.globalParams("0x..."); + +const [count, fee] = await oak.multicall([ + () => gp.getNumberOfListedPlatforms(), + () => gp.getProtocolFeePercent(), +]); +``` + +### Cross-contract batching + +Reads from different entities are batched into one RPC call automatically: + +```typescript +const gp = oak.globalParams("0x..."); +const ci = oak.campaignInfo("0x..."); +const aon = oak.allOrNothingTreasury("0x..."); + +const [platformCount, goalAmount, raisedAmount] = await oak.multicall([ + () => gp.getNumberOfListedPlatforms(), + () => ci.getGoalAmount(), + () => aon.getRaisedAmount(), +]); +``` + +> Under the hood, the SDK enables viem's `batch.multicall` transport option. All `readContract` calls dispatched within the same tick are automatically aggregated into a single Multicall3 on-chain call — no raw ABI descriptors needed. + +> For complete multicall documentation, see: [Multicall](https://oaknetwork.org/docs/contracts-sdk/multicall). + +--- + +## Metrics + +Pre-built aggregation functions that combine multiple on-chain reads into meaningful reports. Import from `@oaknetwork/contracts/metrics`. + +### Platform Stats + +Protocol-level statistics from GlobalParams: + +```typescript +import { getPlatformStats } from "@oaknetwork/contracts/metrics"; + +const stats = await getPlatformStats({ + globalParamsAddress: "0x...", + publicClient: oak.publicClient, +}); + +console.log(`${stats.platformCount} platforms enlisted`); +console.log(`Protocol fee: ${stats.protocolFeePercent} bps`); +``` + +### Campaign Summary + +Financial aggregation from a deployed CampaignInfo contract: + +```typescript +import { getCampaignSummary } from "@oaknetwork/contracts/metrics"; + +const summary = await getCampaignSummary({ + campaignInfoAddress: "0x...", + publicClient: oak.publicClient, +}); + +console.log(`Total raised: ${summary.totalRaised}`); +console.log(`Goal: ${summary.goalAmount}`); +console.log(`Goal reached: ${summary.goalReached}`); +console.log(`Refunded: ${summary.totalRefunded}`); +``` + +### Treasury Report + +Per-treasury financial report for any treasury type: + +```typescript +import { getTreasuryReport } from "@oaknetwork/contracts/metrics"; + +const report = await getTreasuryReport({ + treasuryAddress: "0x...", + treasuryType: "all-or-nothing", // or "keep-whats-raised" | "payment-treasury" + publicClient: oak.publicClient, +}); + +console.log(`Raised: ${report.raisedAmount}`); +console.log(`Refunded: ${report.refundedAmount}`); +console.log(`Fee: ${report.platformFeePercent} bps`); +console.log(`Cancelled: ${report.cancelled}`); +``` + +> For complete metrics documentation, see: [Metrics](https://oaknetwork.org/docs/contracts-sdk/metrics). + +--- + ## Error Handling Contract calls can revert with on-chain errors. The SDK decodes raw revert data into typed error classes with decoded arguments and human-readable recovery hints. @@ -519,6 +631,7 @@ import { getCurrentTimestamp, addDays, getChainFromId, + multicall, createJsonRpcProvider, createWallet, createBrowserProvider, @@ -555,13 +668,14 @@ For complete guidelines on utility functions, please refer to the following link ## Exported Entry Points -| Entry point | Contents | -| --------------------------------- | ------------------------------------------- | -| `@oaknetwork/contracts` | Everything — client, types, utils, errors | -| `@oaknetwork/contracts/utils` | Utility functions only (no client) | -| `@oaknetwork/contracts/contracts` | Contract entity factories only | -| `@oaknetwork/contracts/client` | `createOakContractsClient` only | -| `@oaknetwork/contracts/errors` | Error classes and `parseContractError` only | +| Entry point | Contents | +| --------------------------------- | ------------------------------------------------------------- | +| `@oaknetwork/contracts` | Everything — client, types, utils, errors, multicall | +| `@oaknetwork/contracts/utils` | Utility functions and `multicall` (no client) | +| `@oaknetwork/contracts/contracts` | Contract entity factories only | +| `@oaknetwork/contracts/client` | `createOakContractsClient` only | +| `@oaknetwork/contracts/errors` | Error classes and `parseContractError` only | +| `@oaknetwork/contracts/metrics` | `getPlatformStats`, `getCampaignSummary`, `getTreasuryReport` | --- @@ -623,6 +737,8 @@ See [CLAUDE.md](../../CLAUDE.md) for coding standards including architecture pri - [Full docs](https://oaknetwork.org/docs/contracts-sdk/overview) — oaknetwork.org/docs/sdk/overview - [Quickstart](https://oaknetwork.org/docs/contracts-sdk/quickstart) — oaknetwork.org/docs/sdk/quickstart +- [Multicall](https://oaknetwork.org/docs/contracts-sdk/multicall) — batch reads into one RPC call +- [Metrics](https://oaknetwork.org/docs/contracts-sdk/metrics) — pre-built aggregation reports - [Monorepo README](../../README.md) — README.md - [Changelog](./CHANGELOG.md) — CHANGELOG.md From a23cb0577cf72b03617fe4929476b0416e0eaef8 Mon Sep 17 00:00:00 2001 From: Mahabub Alahi Date: Thu, 2 Apr 2026 15:59:40 +0600 Subject: [PATCH 19/24] Add Multicall3 Address to Celo Chain Definitions - Introduced a constant for the canonical Multicall3 address, ensuring consistency across supported chains. - Updated Celo Mainnet and Celo Sepolia chain definitions to include the Multicall3 contract address and block creation details, enhancing interoperability for multicall functionalities. --- packages/contracts/src/utils/chain.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/contracts/src/utils/chain.ts b/packages/contracts/src/utils/chain.ts index b505b6f1..53693a42 100644 --- a/packages/contracts/src/utils/chain.ts +++ b/packages/contracts/src/utils/chain.ts @@ -2,12 +2,21 @@ import { defineChain } from "../lib"; import { mainnet, sepolia, goerli } from "../lib"; import type { Chain } from "../lib"; +/** + * Canonical Multicall3 address deployed via CREATE2 on all supported chains. + * @see https://www.multicall3.com/deployments + */ +const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11" as const; + /** Celo Mainnet chain definition. */ const celoMainnet = defineChain({ id: 42220, name: "Celo", nativeCurrency: { decimals: 18, name: "CELO", symbol: "CELO" }, rpcUrls: { default: { http: ["https://forno.celo.org"] } }, + contracts: { + multicall3: { address: MULTICALL3_ADDRESS, blockCreated: 13112599 }, + }, }); /** Celo Sepolia testnet chain definition. */ @@ -16,6 +25,9 @@ const celoSepolia = defineChain({ name: "Celo Sepolia", nativeCurrency: { decimals: 18, name: "CELO", symbol: "CELO" }, rpcUrls: { default: { http: ["https://forno.celo-sepolia.celo-testnet.org"] } }, + contracts: { + multicall3: { address: MULTICALL3_ADDRESS, blockCreated: 1 }, + }, }); const CHAIN_REGISTRY: Record = { From 7c651c3f875037959f323e03bfb60522a40dc63d Mon Sep 17 00:00:00 2001 From: tahseen-ccprotocol Date: Thu, 2 Apr 2026 22:40:36 +0600 Subject: [PATCH 20/24] chore: added changeset --- .changeset/lemon-rice-cross.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-rice-cross.md diff --git a/.changeset/lemon-rice-cross.md b/.changeset/lemon-rice-cross.md new file mode 100644 index 00000000..6d0d8eb5 --- /dev/null +++ b/.changeset/lemon-rice-cross.md @@ -0,0 +1,5 @@ +--- +"@oaknetwork/contracts": minor +--- + +Add multicall utility and metrics aggregation module From 27c0fd11f0fc70634837dbda2bd1ed59092e8b84 Mon Sep 17 00:00:00 2001 From: tahseen-ccprotocol Date: Thu, 2 Apr 2026 22:41:48 +0600 Subject: [PATCH 21/24] chore: added changeset --- .changeset/light-glasses-build.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/light-glasses-build.md diff --git a/.changeset/light-glasses-build.md b/.changeset/light-glasses-build.md new file mode 100644 index 00000000..141955c3 --- /dev/null +++ b/.changeset/light-glasses-build.md @@ -0,0 +1,5 @@ +--- +"@oaknetwork/contracts": minor +--- + +Add comprehensive event log handling across all contract entities From c61b6e01c4da532dbcda1f2049bd1fd58a0bce6a Mon Sep 17 00:00:00 2001 From: tahseen-ccprotocol Date: Thu, 2 Apr 2026 23:00:20 +0600 Subject: [PATCH 22/24] chore: fix readme and changeset --- .changeset/lemon-rice-cross.md | 2 +- .changeset/light-glasses-build.md | 2 +- packages/contracts/README.md | 59 ++++++++++++++++--------------- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/.changeset/lemon-rice-cross.md b/.changeset/lemon-rice-cross.md index 6d0d8eb5..f9a22a6e 100644 --- a/.changeset/lemon-rice-cross.md +++ b/.changeset/lemon-rice-cross.md @@ -1,5 +1,5 @@ --- -"@oaknetwork/contracts": minor +"@oaknetwork/contracts-sdk": minor --- Add multicall utility and metrics aggregation module diff --git a/.changeset/light-glasses-build.md b/.changeset/light-glasses-build.md index 141955c3..79019d65 100644 --- a/.changeset/light-glasses-build.md +++ b/.changeset/light-glasses-build.md @@ -1,5 +1,5 @@ --- -"@oaknetwork/contracts": minor +"@oaknetwork/contracts-sdk": minor --- Add comprehensive event log handling across all contract entities diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 9adaf2b1..1487db68 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -461,24 +461,6 @@ await ir.addItemsBatch(itemIds, items); --- -## Multicall - -Batch multiple entity read calls into a single RPC round-trip via the on-chain Multicall3 contract. Pass an array of lazy closures — the same entity read methods you'd normally `await` individually. - -### Standalone utility - -```typescript -import { multicall } from "@oaknetwork/contracts"; - -const gp = oak.globalParams("0x..."); - -const [platformCount, feePercent, admin] = await multicall([ - () => gp.getNumberOfListedPlatforms(), - () => gp.getProtocolFeePercent(), - () => gp.getProtocolAdminAddress(), -]); -``` - ### Client convenience method ```typescript @@ -495,8 +477,8 @@ const [count, fee] = await oak.multicall([ Reads from different entities are batched into one RPC call automatically: ```typescript -const gp = oak.globalParams("0x..."); -const ci = oak.campaignInfo("0x..."); +const gp = oak.globalParams("0x..."); +const ci = oak.campaignInfo("0x..."); const aon = oak.allOrNothingTreasury("0x..."); const [platformCount, goalAmount, raisedAmount] = await oak.multicall([ @@ -570,6 +552,7 @@ console.log(`Cancelled: ${report.cancelled}`); ``` > For complete metrics documentation, see: [Metrics](https://oaknetwork.org/docs/contracts-sdk/metrics). + ## Events Every contract entity exposes an `events` property with three capabilities: @@ -590,7 +573,7 @@ const logs = await gp.events.getPlatformEnlistedLogs(); for (const log of logs) { console.log(log.eventName); // "PlatformEnlisted" - console.log(log.args); // { platformHash: "0x...", adminAddress: "0x...", ... } + console.log(log.args); // { platformHash: "0x...", adminAddress: "0x...", ... } } // Filter by block range @@ -814,7 +797,7 @@ import type { // EventFilterOptions — optional block range for get*Logs interface EventFilterOptions { fromBlock?: bigint; // defaults to 0n (genesis) if omitted - toBlock?: bigint; // defaults to latest block if omitted + toBlock?: bigint; // defaults to latest block if omitted } // DecodedEventLog — returned by get*Logs and decodeLog @@ -932,15 +915,33 @@ For complete guidelines on utility functions, please refer to the following link ## Exported Entry Points -| Entry point | Contents | -| --------------------------------- | ------------------------------------------- | -| `@oaknetwork/contracts-sdk` | Everything — client, types, utils, errors | -| `@oaknetwork/contracts-sdk/utils` | Utility functions only (no client) | -| `@oaknetwork/contracts-sdk/contracts` | Contract entity factories only | -| `@oaknetwork/contracts-sdk/client` | `createOakContractsClient` only | -| `@oaknetwork/contracts-sdk/errors` | Error classes and `parseContractError` only | +| Entry point | Contents | +| ------------------------------------- | ------------------------------------------------------------------------------ | +| `@oaknetwork/contracts-sdk` | Everything — client, types, utils, errors | +| `@oaknetwork/contracts-sdk/utils` | Utility functions only (no client) | +| `@oaknetwork/contracts-sdk/contracts` | Contract entity factories only | +| `@oaknetwork/contracts-sdk/client` | `createOakContractsClient` only | +| `@oaknetwork/contracts-sdk/errors` | Error classes and `parseContractError` only | | `@oaknetwork/contracts-sdk/metrics` | Platform, campaign, and treasury reporting helpers (not re-exported from root) | +## Multicall + +Batch multiple entity read calls into a single RPC round-trip via the on-chain Multicall3 contract. Pass an array of lazy closures — the same entity read methods you'd normally `await` individually. + +### Standalone utility + +```typescript +import { multicall } from "@oaknetwork/contracts"; + +const gp = oak.globalParams("0x..."); + +const [platformCount, feePercent, admin] = await multicall([ + () => gp.getNumberOfListedPlatforms(), + () => gp.getProtocolFeePercent(), + () => gp.getProtocolAdminAddress(), +]); +``` + --- ## Local Development & Testing From 65a939a4ce193030b9e50729e4491f220b1fefcd Mon Sep 17 00:00:00 2001 From: tahseen-ccprotocol Date: Thu, 2 Apr 2026 23:05:25 +0600 Subject: [PATCH 23/24] chore: fix old contract sdk naming --- packages/contracts/README.md | 14 +++++++------- packages/contracts/src/metrics/index.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 1487db68..52c98e14 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -496,14 +496,14 @@ const [platformCount, goalAmount, raisedAmount] = await oak.multicall([ ## Metrics -Pre-built aggregation functions that combine multiple on-chain reads into meaningful reports. Import from `@oaknetwork/contracts/metrics`. +Pre-built aggregation functions that combine multiple on-chain reads into meaningful reports. Import from `@oaknetwork/contracts-sdk/metrics`. ### Platform Stats Protocol-level statistics from GlobalParams: ```typescript -import { getPlatformStats } from "@oaknetwork/contracts/metrics"; +import { getPlatformStats } from "@oaknetwork/contracts-sdk/metrics"; const stats = await getPlatformStats({ globalParamsAddress: "0x...", @@ -519,7 +519,7 @@ console.log(`Protocol fee: ${stats.protocolFeePercent} bps`); Financial aggregation from a deployed CampaignInfo contract: ```typescript -import { getCampaignSummary } from "@oaknetwork/contracts/metrics"; +import { getCampaignSummary } from "@oaknetwork/contracts-sdk/metrics"; const summary = await getCampaignSummary({ campaignInfoAddress: "0x...", @@ -537,7 +537,7 @@ console.log(`Refunded: ${summary.totalRefunded}`); Per-treasury financial report for any treasury type: ```typescript -import { getTreasuryReport } from "@oaknetwork/contracts/metrics"; +import { getTreasuryReport } from "@oaknetwork/contracts-sdk/metrics"; const report = await getTreasuryReport({ treasuryAddress: "0x...", @@ -784,7 +784,7 @@ const unwatch = ir.events.watchItemAdded(handler); ### Types -All event methods use shared types from `@oaknetwork/contracts`: +All event methods use shared types from `@oaknetwork/contracts-sdk`: ```typescript import type { @@ -792,7 +792,7 @@ import type { EventFilterOptions, EventWatchHandler, RawLog, -} from "@oaknetwork/contracts"; +} from "@oaknetwork/contracts-sdk"; // EventFilterOptions — optional block range for get*Logs interface EventFilterOptions { @@ -931,7 +931,7 @@ Batch multiple entity read calls into a single RPC round-trip via the on-chain M ### Standalone utility ```typescript -import { multicall } from "@oaknetwork/contracts"; +import { multicall } from "@oaknetwork/contracts-sdk"; const gp = oak.globalParams("0x..."); diff --git a/packages/contracts/src/metrics/index.ts b/packages/contracts/src/metrics/index.ts index 14f9fe37..e21a4a2f 100644 --- a/packages/contracts/src/metrics/index.ts +++ b/packages/contracts/src/metrics/index.ts @@ -1,6 +1,6 @@ /** * @file metrics/index.ts - * Public surface for the `@oaknetwork/contracts/metrics` sub-path export. + * Public surface for the `@oaknetwork/contracts-sdk/metrics` sub-path export. * * - platform.ts — protocol-level statistics from GlobalParams. * - campaign.ts — campaign-level financial aggregation from CampaignInfo. From 476464a7b663248c5b375d590774deae354b355a Mon Sep 17 00:00:00 2001 From: tahseen-ccprotocol Date: Thu, 2 Apr 2026 23:39:44 +0600 Subject: [PATCH 24/24] chore: fix readme links --- packages/contracts/README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 52c98e14..c0c869b5 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -8,7 +8,7 @@ TypeScript SDK for interacting with Oak Network smart contracts. Provides a type > **You need deployed contract addresses to use this SDK.** -> The SDK interacts with Oak Network smart contracts that must already be deployed on-chain. To get your contract addresses and sandbox environment access, contact our team at **support@oaknetwork.org**. +> The SDK interacts with Oak Network smart contracts that must already be deployed on-chain. To get your contract addresses and sandbox environment access, contact our team at **[support@oaknetwork.org](mailto:support@oaknetwork.org)**. ## Installation @@ -335,11 +335,13 @@ Handles fiat-style payments via a payment gateway. Manages payment creation, con > **Two treasury variants, one SDK method.** The `paymentTreasury()` method works with both on-chain implementations: > +> > | Variant | Description | > | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | > | **PaymentTreasury** | Standard payment treasury with no time restrictions. Payments can be created, confirmed, and refunded at any time while the treasury is active. | > | **TimeConstrainedPaymentTreasury** | Time-constrained variant that enforces launch-time and deadline windows on-chain. Payments can only be created within the campaign window (launch → deadline + buffer). Refunds, withdrawals, and fee disbursements are only available after launch. | > +> > Both contracts share the same ABI and the same SDK interface. Time enforcement is handled entirely on-chain — simply pass the deployed contract address regardless of which variant was deployed: ```typescript @@ -915,6 +917,7 @@ For complete guidelines on utility functions, please refer to the following link ## Exported Entry Points + | Entry point | Contents | | ------------------------------------- | ------------------------------------------------------------------------------ | | `@oaknetwork/contracts-sdk` | Everything — client, types, utils, errors | @@ -924,6 +927,7 @@ For complete guidelines on utility functions, please refer to the following link | `@oaknetwork/contracts-sdk/errors` | Error classes and `parseContractError` only | | `@oaknetwork/contracts-sdk/metrics` | Platform, campaign, and treasury reporting helpers (not re-exported from root) | + ## Multicall Batch multiple entity read calls into a single RPC round-trip via the on-chain Multicall3 contract. Pass an array of lazy closures — the same entity read methods you'd normally `await` individually. @@ -990,20 +994,18 @@ See [CLAUDE.md](../../CLAUDE.md) for coding standards including architecture pri ### Code review checklist -- [ ] `pnpm build` succeeds -- [ ] `pnpm test` passes with >90% coverage -- [ ] `pnpm lint` has no errors -- [ ] Changeset created with `pnpm changeset` -- [ ] Documentation updated if needed +- `pnpm build` succeeds +- `pnpm test` passes with >90% coverage +- `pnpm lint` has no errors +- Changeset created with `pnpm changeset` +- Documentation updated if needed --- ## Documentation -- [Full docs](https://oaknetwork.org/docs/contracts-sdk/overview) — oaknetwork.org/docs/sdk/overview -- [Quickstart](https://oaknetwork.org/docs/contracts-sdk/quickstart) — oaknetwork.org/docs/sdk/quickstart -- [Multicall](https://oaknetwork.org/docs/contracts-sdk/multicall) — batch reads into one RPC call -- [Metrics](https://oaknetwork.org/docs/contracts-sdk/metrics) — pre-built aggregation reports +- [Full docs](https://oaknetwork.org/docs/contracts-sdk/overview) — oaknetwork.org/docs/contracts-sdk/overview +- [Quickstart](https://oaknetwork.org/docs/contracts-sdk/quickstart) — oaknetwork.org/docs/contracts-sdk/quickstart - [Monorepo README](../../README.md) — README.md - [Changelog](./CHANGELOG.md) — CHANGELOG.md @@ -1027,4 +1029,4 @@ See [CLAUDE.md](../../CLAUDE.md) for coding standards including architecture pri - [Issues](https://github.com/oak-network/sdk/issues) - [npm](https://www.npmjs.com/package/@oaknetwork/contracts-sdk) -Questions? [Open an issue](https://github.com/oak-network/sdk/issues) or contact **support@oaknetwork.org** +Questions? [Open an issue](https://github.com/oak-network/sdk/issues) or contact **[support@oaknetwork.org](mailto:support@oaknetwork.org)** \ No newline at end of file