diff --git a/.changeset/lemon-rice-cross.md b/.changeset/lemon-rice-cross.md new file mode 100644 index 00000000..f9a22a6e --- /dev/null +++ b/.changeset/lemon-rice-cross.md @@ -0,0 +1,5 @@ +--- +"@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 new file mode 100644 index 00000000..79019d65 --- /dev/null +++ b/.changeset/light-glasses-build.md @@ -0,0 +1,5 @@ +--- +"@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 1ab458c8..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 @@ -461,6 +463,365 @@ await ir.addItemsBatch(itemIds, items); --- +### 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-sdk/metrics`. + +### Platform Stats + +Protocol-level statistics from GlobalParams: + +```typescript +import { getPlatformStats } from "@oaknetwork/contracts-sdk/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-sdk/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-sdk/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). + +## 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-sdk`: + +```typescript +import type { + DecodedEventLog, + EventFilterOptions, + EventWatchHandler, + RawLog, +} from "@oaknetwork/contracts-sdk"; + +// 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. @@ -519,6 +880,7 @@ import { getCurrentTimestamp, addDays, getChainFromId, + multicall, createJsonRpcProvider, createWallet, createBrowserProvider, @@ -555,15 +917,35 @@ 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-sdk"; + +const gp = oak.globalParams("0x..."); + +const [platformCount, feePercent, admin] = await multicall([ + () => gp.getNumberOfListedPlatforms(), + () => gp.getProtocolFeePercent(), + () => gp.getProtocolAdminAddress(), +]); +``` + --- ## Local Development & Testing @@ -612,11 +994,11 @@ 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 --- @@ -647,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 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/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/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 })); + }); + }); }); // ============================================================ 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); }); }); 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: [ 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. */ diff --git a/packages/contracts/src/contracts/all-or-nothing/events.ts b/packages/contracts/src/contracts/all-or-nothing/events.ts index 1330a58e..87f690d5 100644 --- a/packages/contracts/src/contracts/all-or-nothing/events.ts +++ b/packages/contracts/src/contracts/all-or-nothing/events.ts @@ -1,18 +1,161 @@ -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); + }, + 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); + }, + watchWithdrawalSuccessful(onLogs) { + return createWatcher(publicClient, address, "WithdrawalSuccessful", onLogs); + }, + 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 52c0ce19..9e63cf66 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,58 @@ 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; + /** 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. */ + 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; + /** 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. */ export type AllOrNothingTreasuryEntity = AllOrNothingReads & AllOrNothingWrites & { 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 & { 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 & { 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 & { 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 & 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 & 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). 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 & 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/lib/viem/provider.ts b/packages/contracts/src/lib/viem/provider.ts index d0621420..a71e7a04 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/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 e073f873..e21a4a2f 100644 --- a/packages/contracts/src/metrics/index.ts +++ b/packages/contracts/src/metrics/index.ts @@ -1,9 +1,21 @@ /** * @file metrics/index.ts - * Public surface for the @oaknetwork/contracts-sdk/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. + * - 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/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"; 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 = { 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> }; +}