Skip to content

Comments

Snapshot implementation#530

Merged
FonsSicca merged 3 commits intomainfrom
dan/snapshot-implementation
Feb 20, 2026
Merged

Snapshot implementation#530
FonsSicca merged 3 commits intomainfrom
dan/snapshot-implementation

Conversation

@FonsSicca
Copy link
Contributor

@FonsSicca FonsSicca commented Feb 18, 2026

closes #454
closes #484

Implements:

  • Integration of snapshot logic implementation into appropriate event handlers/aggregator modules

Summary by CodeRabbit

  • Bug Fixes

    • Improved snapshot timing accuracy: snapshots are now epoch-aligned and no longer include a blockNumber field, yielding more consistent historical data.
  • Refactor

    • Snapshot creation was separated from persistence and centralized, simplifying snapshot workflows and ensuring consistent timestamp propagation across events.
  • Tests

    • Expanded snapshot test coverage to validate epoch alignment and persistence behavior.

@FonsSicca FonsSicca requested a review from WooSungD February 18, 2026 15:23
@FonsSicca FonsSicca self-assigned this Feb 18, 2026
@FonsSicca FonsSicca changed the base branch from main to dan/snapshots-initial-setup February 18, 2026 15:24
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Walkthrough

Refactors snapshot system to epoch-aligned timestamps, removes blockNumber from several snapshot types, separates snapshot creation from persistence (create*/set* pattern), adds centralized snapshot persistence utilities, and propagates timestamps through aggregators and event handlers to drive snapshotting.

Changes

Cohort / File(s) Summary
Schema Changes
schema.graphql
Removed blockNumber: Int! from UserStatsPerPoolSnapshot, NonFungiblePositionSnapshot, VeNFTStateSnapshot, and ALM_LP_WrapperSnapshot.
Snapshot Core / Shared
src/Snapshots/Shared.ts, src/Snapshots/Index.ts
Added SnapshotType enum, SnapshotForPersist type, getSnapshotEpoch(), and persistSnapshot(); expanded index exports to expose create/set pairs and Shared utilities.
Snapshot Modules (create + set)
src/Snapshots/*Snapshot.ts
Refactored snapshot modules to provide create*Snapshot(entity, timestamp) (pure) and set*Snapshot(entity, timestamp, context) (persists via persistSnapshot). Removed blockNumber from creation paths and align timestamps to epoch.
Aggregators — snapshot integration & timestamps
src/Aggregators/{UserStatsPerPool.ts,NonFungiblePosition.ts,VeNFTState.ts,ALMLPWrapper.ts}
Added timestamp parameter to update functions (where applicable), introduced shouldSnapshot/getSnapshotEpoch checks, call set*Snapshot on epoch boundaries, and added lastSnapshotTimestamp to UserStatsPerPool (init undefined).
Event handlers — timestamp propagation
src/EventHandlers/** (CLPool, Pool, NFPM/, ALM/, Gauges/, VeNFT/, Voter/, VotingReward/)
Propagated a centralized per-event timestamp (from event.block.timestamp) to aggregator calls; updated numerous call sites to pass new timestamp parameter.
Price oracle
src/PriceOracle.ts
Replaced inline TokenPriceSnapshot creation with createTokenPriceSnapshot/setTokenPriceSnapshot usage consistent with new snapshot API.
Snapshot persistence flow
src/Snapshots/*
Centralized persisting by wrapping created snapshots into SnapshotForPersist and switching on SnapshotType inside persistSnapshot to write to context stores.
Tests
test/**, test/Snapshots/**
Updated and extended tests: added createSnapshot tests (epoch/id/field-copy), adapted setSnapshot signatures (removed blockNumber), added mocks for snapshot stores in handlerContext across many tests, and assertions verifying snapshot writes and lastSnapshotTimestamp behavior.

Sequence Diagram(s)

sequenceDiagram
  participant EventHandler
  participant Aggregator
  participant SnapshotShared
  participant ContextStore

  EventHandler->>Aggregator: call update*(diff, current, context, timestamp)
  Aggregator->>Aggregator: apply diffs, compute updated entity
  Aggregator->>SnapshotShared: shouldSnapshot(lastSnapshotTimestamp, timestamp)?
  alt should snapshot
    Aggregator->>SnapshotShared: create*Snapshot(entity, timestamp) -> snapshot
    Aggregator->>SnapshotShared: wrap SnapshotForPersist(type, snapshot)
    SnapshotShared->>ContextStore: persistSnapshot(snapshotForPersist, context)
    Aggregator->>Aggregator: set lastSnapshotTimestamp = getSnapshotEpoch(timestamp)
  end
  Aggregator->>ContextStore: write updated entity/state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • WooSungD

Poem

🐰
I hop through code at break of dawn,
Epochs tick—old blocks are gone.
Create a snapshot, then persist with care,
Timestamps aligned—a tidy lair!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Snapshot implementation' accurately reflects the main objective of the PR: implementing snapshot integration across multiple entities and event handlers.
Linked Issues check ✅ Passed The PR comprehensively addresses both linked issues: #454 (extends snapshots to user, ALM LP wrapper, VeNFT, NFT positions) and #484 (prepares snapshots for token holdings tracking).
Out of Scope Changes check ✅ Passed All changes are directly related to snapshot implementation objectives: snapshot infrastructure (Shared.ts), snapshot functions for each entity type, event handler integration, and corresponding test updates.
Docstring Coverage ✅ Passed Docstring coverage is 92.11% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dan/snapshot-implementation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@FonsSicca FonsSicca changed the title Dan/snapshot implementation Snapshot implementation Feb 18, 2026
Comment on lines +43 to +50
if (shouldSnapshot(current.lastSnapshotTimestamp, timestamp)) {
setALMLPWrapperSnapshot(updated, timestamp, context);
updated = {
...updated,
lastSnapshotTimestamp: getSnapshotEpoch(timestamp),
};
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALM wrapper snapshot implementation

Comment on lines +50 to +56
if (shouldSnapshot(current.lastSnapshotTimestamp, timestamp)) {
setNonFungiblePositionSnapshot(nonFungiblePosition, timestamp, context);
nonFungiblePosition = {
...nonFungiblePosition,
lastSnapshotTimestamp: getSnapshotEpoch(timestamp),
};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NFP snapshot implementation

Comment on lines +385 to +392
if (shouldSnapshot(current.lastSnapshotTimestamp, timestamp)) {
setUserStatsPerPoolSnapshot(updated, timestamp, context);
updated = {
...updated,
lastSnapshotTimestamp: getSnapshotEpoch(timestamp),
};
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User snapshot implementation

Comment on lines +66 to +73

if (shouldSnapshot(current.lastSnapshotTimestamp, timestamp)) {
setVeNFTStateSnapshot(veNFTState, timestamp, context);
veNFTState = {
...veNFTState,
lastSnapshotTimestamp: getSnapshotEpoch(timestamp),
};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

veNF snapshot implementation

Comment on lines +159 to +167
setTokenPriceSnapshot(
token.address,
chainId,
blockNumber,
new Date(blockTimestampMs),
currentPrice,
token.isWhitelisted,
context,
);
Copy link
Contributor Author

@FonsSicca FonsSicca Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token snapshot implementation

};

if (shouldSnapshot(current.lastSnapshotTimestamp, timestamp)) {
setNonFungiblePositionSnapshot(nonFungiblePosition, timestamp, context);
Copy link
Contributor

@WooSungD WooSungD Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[discussion; non-blocking] making the current set function into a get function and setting the context inside this function could be better for future debugging i.e. all context.<>.set happens in one place.

Example

    nonFungiblePositionSnapshot = getNonFungiblePositionSnapshot(nonFungiblePosition, timestamp, context);
    context.NonFungiblePositionSnapshot.set(nonFungiblePositionSnapshot);   

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this commit following your suggestion

ee5de64

@FonsSicca FonsSicca force-pushed the dan/snapshots-initial-setup branch from 0f55a70 to 2ffe329 Compare February 19, 2026 10:26
Base automatically changed from dan/snapshots-initial-setup to main February 19, 2026 10:29
@FonsSicca FonsSicca force-pushed the dan/snapshot-implementation branch from 207cbcf to b6c4569 Compare February 19, 2026 10:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (13)
test/EventHandlers/Pool/PoolBurnAndMintLogic.test.ts (1)

38-38: new Date() in a fixture violates the test guideline — prefer a fixed timestamp from a shared helper.

TIMESTAMP_DATE is constructed with new Date(TIMESTAMP * 1000) and is passed directly to processPoolLiquidityEvent, which now drives the new epoch-aligned snapshot logic. The guideline says to avoid new Date() in fixtures unless using fake timers and to prefer fixed timestamps from shared helpers.

♻️ Suggested refactor
- const TIMESTAMP = 1000000;
- const TIMESTAMP_DATE = new Date(TIMESTAMP * 1000);
+ const TIMESTAMP = 1000000;
+ // Derive a stable Date from the fixed epoch constant instead of calling new Date() directly.
+ const TIMESTAMP_DATE = new Date(1000000 * 1000); // or import from a shared helper

If common.ts (or another shared helper) already exports a canonical TIMESTAMP/TIMESTAMP_DATE, import and reuse it here instead.

As per coding guidelines: "Avoid new Date() in assertions and fixtures unless using fake timers; prefer fixed timestamps from shared helpers."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/EventHandlers/Pool/PoolBurnAndMintLogic.test.ts` at line 38, Replace the
local construction of TIMESTAMP_DATE using new Date(TIMESTAMP * 1000) with the
canonical fixed timestamp/date provided by the shared test helper (e.g., import
and use the exported TIMESTAMP or TIMESTAMP_DATE from common.ts), and pass that
fixed value into processPoolLiquidityEvent (reference: TIMESTAMP_DATE and
processPoolLiquidityEvent) so the test uses the shared, deterministic timestamp
fixture instead of constructing a Date inside the test.
src/EventHandlers/VotingReward/BribesVotingReward.ts (1)

52-57: Timestamp propagation is correct.

The new Date(data.timestamp * 1000) conversion matches the pattern used at line 45 for the pool update. Minor note: both lines create separate Date instances for the same timestamp — could be extracted to a shared const timestamp = new Date(data.timestamp * 1000) at the top of the handler for clarity, but this is cosmetic.

♻️ Optional: extract shared timestamp
+  const timestamp = new Date(data.timestamp * 1000);
+
   await Promise.all([
     result.poolDiff
       ? updateLiquidityPoolAggregator(
           result.poolDiff,
           loadedData.poolData.liquidityPoolAggregator,
-          new Date(data.timestamp * 1000),
+          timestamp,
           context,
           event.chainId,
           data.blockNumber,
         )
       : Promise.resolve(),
     result.userDiff
       ? updateUserStatsPerPool(
           result.userDiff,
           loadedData.userData,
           context,
-          new Date(data.timestamp * 1000),
+          timestamp,
         )
       : Promise.resolve(),
   ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EventHandlers/VotingReward/BribesVotingReward.ts` around lines 52 - 57,
The two places creating Date objects for the same timestamp (the call to
updateUserStatsPerPool and the earlier pool update around line 45) should reuse
a single Date instance for clarity; extract const timestamp = new
Date(data.timestamp * 1000) at the start of the handler and pass timestamp to
updateUserStatsPerPool (and to the pool update call) instead of creating new
Date(...) inline.
test/Aggregators/VeNFTState.test.ts (1)

259-279: Prefer getSnapshotEpoch over hardcoded interval for the lastSnapshotTimestamp assertion.

Line 262 hardcodes 60 * 60 * 1000 as the snapshot interval, while line 266 correctly uses getSnapshotEpoch(timestamp). If SNAPSHOT_INTERVAL_IN_MS ever changes, the assertion on line 262 would silently drift. Consider using getSnapshotEpoch for both assertions for consistency.

♻️ Suggested change
-        expect(updated.lastSnapshotTimestamp?.getTime()).toBe(
-          Math.floor(timestamp.getTime() / (60 * 60 * 1000)) * (60 * 60 * 1000),
-        );
+        const expectedEpoch = getSnapshotEpoch(timestamp);
+        expect(updated.lastSnapshotTimestamp?.getTime()).toBe(
+          expectedEpoch.getTime(),
+        );

Then the const expectedEpoch declaration on line 266 can be removed since it's already defined above.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Aggregators/VeNFTState.test.ts` around lines 259 - 279, The first
assertion that checks updated.lastSnapshotTimestamp currently computes the epoch
with a hardcoded 60*60*1000; change it to use getSnapshotEpoch(timestamp) (i.e.,
compare updated.lastSnapshotTimestamp or its .getTime() to expectedEpoch or
expectedEpoch.getTime()) so both assertions use the same helper; remove the
redundant const expectedEpoch if you end up reusing the one declared later and
ensure the VeNFTStateSnapshot.set expectation continues to use
VeNFTStateSnapshotId, chainId, tokenId, owner, locktime, lastUpdatedTimestamp,
totalValueLocked, isAlive and timestamp as before.
src/EventHandlers/NFPM/NFPMDecreaseLiquidityLogic.ts (1)

75-81: Optional: redundant new Date(event.block.timestamp * 1000) construction.

calculateDecreaseLiquidityDiff already computes const blockDatetime = new Date(event.block.timestamp * 1000) internally (line 24) and stores it in the diff's lastUpdatedTimestamp. The new timestamp at line 75 creates a second, identical Date object. Consider returning the Date from calculateDecreaseLiquidityDiff or accepting it as a parameter so both usages share a single object.

♻️ Example refactor
-export function calculateDecreaseLiquidityDiff(
-  event: NFPM_DecreaseLiquidity_event,
-): Partial<NonFungiblePositionDiff> {
-  const blockDatetime = new Date(event.block.timestamp * 1000);
-  ...
-  return nonFungiblePositionDiff;
-}
+export function calculateDecreaseLiquidityDiff(
+  event: NFPM_DecreaseLiquidity_event,
+  timestamp: Date,
+): Partial<NonFungiblePositionDiff> {
+  ...
+  const nonFungiblePositionDiff = {
+    incrementalLiquidity: -event.params.liquidity,
+    lastUpdatedTimestamp: timestamp,
+  };
+  return nonFungiblePositionDiff;
+}
+  const timestamp = new Date(event.block.timestamp * 1000);
-  const nonFungiblePositionDiff = calculateDecreaseLiquidityDiff(event);
+  const nonFungiblePositionDiff = calculateDecreaseLiquidityDiff(event, timestamp);
-  const timestamp = new Date(event.block.timestamp * 1000);
   updateNonFungiblePosition(nonFungiblePositionDiff, position, context, timestamp);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EventHandlers/NFPM/NFPMDecreaseLiquidityLogic.ts` around lines 75 - 81,
The code creates a duplicate Date by calling new Date(event.block.timestamp *
1000) before updateNonFungiblePosition even though
calculateDecreaseLiquidityDiff already computed and stored the timestamp in
nonFungiblePositionDiff.lastUpdatedTimestamp; change the call site to reuse that
existing Date (nonFungiblePositionDiff.lastUpdatedTimestamp) or adjust
calculateDecreaseLiquidityDiff to return the Date explicitly and then pass that
returned Date into updateNonFungiblePosition instead of constructing a new one
so both uses share the single Date object (references:
calculateDecreaseLiquidityDiff, nonFungiblePositionDiff.lastUpdatedTimestamp,
updateNonFungiblePosition).
test/Aggregators/NonFungiblePosition.test.ts (1)

112-117: Good snapshot epoch assertion — consider adding a "no snapshot within same epoch" test.

The assertion correctly verifies that lastSnapshotTimestamp is set to getSnapshotEpoch(timestamp) and that the snapshot was written. However, all three test cases start with lastSnapshotTimestamp: undefined, so only the "first snapshot" path is covered. A test where lastSnapshotTimestamp is already set to the current epoch (asserting the snapshot is not taken) would strengthen confidence in the shouldSnapshot gate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Aggregators/NonFungiblePosition.test.ts` around lines 112 - 117, Add a
new unit test in NonFungiblePosition.test.ts that seeds
result.lastSnapshotTimestamp to getSnapshotEpoch(timestamp) (i.e., same epoch as
the incoming timestamp) and asserts that no new snapshot is taken: call the same
function/handler used in the other tests with that timestamp, then verify
result.lastSnapshotTimestamp remains unchanged and
mockContext.NonFungiblePositionSnapshot?.set was NOT called; use
getSnapshotEpoch(timestamp) to compute the pre-seeded epoch and reference the
same timestamp/result/mockContext variables used in the existing tests to keep
setup consistent.
test/EventHandlers/VeNFT/VeNFTLogic.test.ts (1)

388-397: Extract repeated timestamp construction to a named constant.

new Date(mockTransferEvent.block.timestamp * 1000) is constructed inline four times in assertions (lines 388, 396, 467, 528). Per coding guidelines, timestamps in fixtures/assertions should use fixed named values rather than inline constructions.

♻️ Proposed refactor
+  const transferTimestamp = new Date(mockTransferEvent.block.timestamp * 1000);
+
   expect(updateUserStatsSpy).toHaveBeenCalledWith(
     expect.objectContaining({
       incrementalVeNFTamountStaked: -50n,
     }),
     previousOwnerStats,
     mockContext,
-    new Date(mockTransferEvent.block.timestamp * 1000),
+    transferTimestamp,
   );
   expect(updateUserStatsSpy).toHaveBeenCalledWith(
     expect.objectContaining({
       incrementalVeNFTamountStaked: 50n,
     }),
     newOwnerStats,
     mockContext,
-    new Date(mockTransferEvent.block.timestamp * 1000),
+    transferTimestamp,
   );

Apply the same extraction in the updatePreviousOwnerUserStatsOnTransfer and updateNewOwnerUserStatsOnTransfer describe blocks (lines 467, 528). Based on learnings, the coding guideline specifies: "Avoid new Date() in assertions and fixtures unless using fake timers; prefer fixed timestamps from shared helpers."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/EventHandlers/VeNFT/VeNFTLogic.test.ts` around lines 388 - 397, The
tests repeatedly construct new Date(mockTransferEvent.block.timestamp * 1000)
inline; extract that into a single named constant (e.g., const
expectedTransferDate = new Date(mockTransferEvent.block.timestamp * 1000)) and
use that constant in all assertions that call updateUserStatsSpy and any other
assertions in the updatePreviousOwnerUserStatsOnTransfer and
updateNewOwnerUserStatsOnTransfer describe blocks so the timestamp is fixed and
reused across expect calls involving updateUserStatsSpy, newOwnerStats, and
related assertions.
src/EventHandlers/Pool.ts (1)

90-138: Optional: extract timestamp before Promise.all to avoid computing new Date(event.block.timestamp * 1000) twice.

In both Pool.Fees (lines 106 and 121) and Pool.Swap (lines 157 and 172), new Date(event.block.timestamp * 1000) is constructed twice — once inline inside loadOrCreateUserData and again as a named timestamp const afterwards. The named const can be hoisted above the Promise.all and reused in both places.

♻️ Proposed refactor for Pool.Fees (same pattern applies to Pool.Swap)
 Pool.Fees.handler(async ({ event, context }) => {
+  const timestamp = new Date(event.block.timestamp * 1000);
   const [poolData, userData] = await Promise.all([
     loadPoolData(
       event.srcAddress,
       event.chainId,
       context,
       event.block.number,
       event.block.timestamp,
     ),
     loadOrCreateUserData(
       event.params.sender,
       event.srcAddress,
       event.chainId,
       context,
-      new Date(event.block.timestamp * 1000),
+      timestamp,
     ),
   ]);
   ...
-  const timestamp = new Date(event.block.timestamp * 1000);
-
   // Update pool and user entities in parallel
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EventHandlers/Pool.ts` around lines 90 - 138, Hoist the computed
timestamp into a const before the Promise.all so it’s created once and reused;
specifically, compute const timestamp = new Date(event.block.timestamp * 1000)
above the Promise.all that calls loadOrCreateUserData and then pass that
timestamp into loadOrCreateUserData (instead of creating a new Date inline) and
reuse the same timestamp when calling updateLiquidityPoolAggregator and
updateUserStatsPerPool; update the Pool.Fees handler (functions:
loadOrCreateUserData, updateLiquidityPoolAggregator, updateUserStatsPerPool)
accordingly so the Date allocation is not duplicated.
test/Aggregators/UserStatsPerPoolLiquidity.test.ts (2)

28-35: UserStatsPerPool.set should be jest.fn() for consistency and debuggability.

async () => {} is an untracked no-op. If a future test needs to assert on UserStatsPerPool.set, it silently provides no call records — inconsistent with UserStatsPerPoolSnapshot: { set: jest.fn() } on the very next line.

♻️ Proposed fix
-      UserStatsPerPool: { set: async () => {} },
+      UserStatsPerPool: { set: jest.fn() },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Aggregators/UserStatsPerPoolLiquidity.test.ts` around lines 28 - 35,
Replace the untracked no-op used for UserStatsPerPool.set with a Jest mock so
tests can assert calls; specifically, in the beforeEach where mockContext is
created via setupCommon(), change UserStatsPerPool: { set: async () => {} } to
use jest.fn() (e.g., UserStatsPerPool: { set: jest.fn() }) so the
mockContext.UserStatsPerPool.set records invocations similar to
UserStatsPerPoolSnapshot.set.

56-65: Snapshot assertion missing from multi-step tests.

Every test calls createMockUserStats() which produces an entity with lastSnapshotTimestamp: undefined. shouldSnapshot(undefined, mockTimestamp) returns true on the first updateUserStatsPerPool call in every test, so UserStatsPerPoolSnapshot.set is triggered in all tests — but only "should handle positive liquidity addition correctly" (line 56) asserts it. Consider adding a snapshot assertion (or at minimum a .not.toHaveBeenCalledTimes(0) check) to the other first-call tests for complete coverage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Aggregators/UserStatsPerPoolLiquidity.test.ts` around lines 56 - 65, The
tests call createMockUserStats() which yields an entity with
lastSnapshotTimestamp: undefined so shouldSnapshot(undefined, mockTimestamp)
returns true on the first updateUserStatsPerPool call and always triggers
UserStatsPerPoolSnapshot.set; update the other tests (besides "should handle
positive liquidity addition correctly") to assert snapshot behavior as
well—either assert UserStatsPerPoolSnapshot.set was called with the expected
snapshot object (using expect.objectContaining with userAddress, poolAddress,
chainId, etc.) or at minimum add a negative/positive call-count check like
expect(mockContext.UserStatsPerPoolSnapshot.set).not.toHaveBeenCalledTimes(0) to
reflect the first-call snapshot; update references to createMockUserStats,
shouldSnapshot, updateUserStatsPerPool, and UserStatsPerPoolSnapshot.set when
adding the assertions.
test/Snapshots/Shared.test.ts (1)

78-227: Extract setupCommon() to beforeEach to avoid repeating the call in every test.

const common = setupCommon() is identically repeated at the top of all 7 persistSnapshot tests. Per the guideline on minimizing duplication, it should be lifted to beforeEach.

♻️ Proposed refactor
 describe("persistSnapshot", () => {
   const oneHourMs = SNAPSHOT_INTERVAL_IN_MS;
+  let common: ReturnType<typeof setupCommon>;
+  beforeEach(() => {
+    common = setupCommon();
+  });

   it("should call LiquidityPoolAggregatorSnapshot.set when type is LiquidityPoolAggregator", () => {
-    const common = setupCommon();
     const context = common.createMockContext({
       LiquidityPoolAggregatorSnapshot: { set: jest.fn() },
     });
     // ... rest of test unchanged

Based on learnings: "Minimize duplication by reusing existing test helpers; if logic repeats across 2+ tests, extract it into a shared helper."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Snapshots/Shared.test.ts` around lines 78 - 227, The tests in the
"persistSnapshot" describe block repeatedly call setupCommon(); extract that
call into a beforeEach that assigns the result to a local variable used by each
test (e.g., declare let common; in the describe and set common = setupCommon()
inside beforeEach). Update each test to remove the const common = setupCommon()
lines and use the shared common variable; keep all existing mocks and assertions
referencing common, context, and persistSnapshot as-is.
src/EventHandlers/CLPool.ts (1)

120-170: CollectFees, Flash, and Swap still create duplicate new Date(event.block.timestamp * 1000) — inconsistent with the Collect refactor.

CLPool.Collect (line 71) correctly hoists timestamp before the Promise.all and reuses it in both loadOrCreateUserData and updateUserStatsPerPool. The other three handlers still pass new Date(event.block.timestamp * 1000) inline to loadOrCreateUserData and then redeclare an identical const timestamp a few lines later.

♻️ Proposed pattern (CollectFees shown; apply same to Flash and Swap)
 CLPool.CollectFees.handler(async ({ event, context }) => {
+  const timestamp = new Date(event.block.timestamp * 1000);
   const [poolData, userData] = await Promise.all([
     loadPoolData(
       event.srcAddress,
       event.chainId,
       context,
       event.block.number,
       event.block.timestamp,
     ),
     loadOrCreateUserData(
       event.params.recipient,
       event.srcAddress,
       event.chainId,
       context,
-      new Date(event.block.timestamp * 1000),
+      timestamp,
     ),
   ]);
   ...
-  const timestamp = new Date(event.block.timestamp * 1000);
-
   // Update pool and user entities
   await Promise.all([

Also applies to: 172-219, 332-407

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EventHandlers/CLPool.ts` around lines 120 - 170, The CollectFees handler
(CLPool.CollectFees.handler) creates duplicate Date objects: it passes new
Date(event.block.timestamp * 1000) into loadOrCreateUserData and then constructs
an identical timestamp later; hoist a single const timestamp = new
Date(event.block.timestamp * 1000) before calling Promise.all so both
loadOrCreateUserData and updateUserStatsPerPool use the same timestamp instance,
and apply the same change to the Flash and Swap handlers (they also call
loadOrCreateUserData with inline new Date(...) and later redeclare timestamp);
update CLPool.CollectFees.handler, CLPool.Flash.handler, and CLPool.Swap.handler
to compute timestamp once and reuse it in both the concurrent
loadOrCreateUserData call and the later
updateUserStatsPerPool/updateLiquidityPoolAggregator calls.
src/Snapshots/LiquidityPoolAggregatorSnapshot.ts (1)

104-117: Nit: inconsistent parameter naming in setLiquidityPoolAggregatorSnapshot.

Other set wrappers (VeNFTState, NonFungiblePosition, UserStatsPerPool, ALMLPWrapper) name the first parameter entity, while this one uses liquidityPoolAggregator. Trivial, but aligning would improve consistency.

♻️ Suggested rename
 export function setLiquidityPoolAggregatorSnapshot(
-  liquidityPoolAggregator: LiquidityPoolAggregator,
+  entity: LiquidityPoolAggregator,
   timestamp: Date,
   context: handlerContext,
 ): void {
   const snapshotForPersist: SnapshotForPersist = {
     type: SnapshotType.LiquidityPoolAggregator,
     snapshot: createLiquidityPoolAggregatorSnapshot(
-      liquidityPoolAggregator,
+      entity,
       timestamp,
     ),
   };
   persistSnapshot(snapshotForPersist, context);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Snapshots/LiquidityPoolAggregatorSnapshot.ts` around lines 104 - 117, The
parameter name for setLiquidityPoolAggregatorSnapshot is inconsistent with other
setters; rename the first parameter liquidityPoolAggregator to entity to match
conventions used in functions like setVeNFTState, setNonFungiblePosition,
setUserStatsPerPool, and setALMLPWrapper; update all references within
setLiquidityPoolAggregatorSnapshot (including the call to
createLiquidityPoolAggregatorSnapshot) and keep the rest of the function logic
unchanged so the SnapshotForPersist construction and persistSnapshot call
continue to work.
test/Aggregators/Users.test.ts (1)

50-68: Consider extracting the repeated Object.assign mock setup into a helper.

Every test case within updateUserStatsPerPool repeats the same Object.assign(mockContext.UserStatsPerPool, { set: ... }) pattern with the same savedUserStats capture variable. A small helper (e.g., captureUserStats()) could reduce the boilerplate and improve readability.

♻️ Example helper extraction
function captureSet(mockContext: handlerContext) {
  let saved: UserStatsPerPool | undefined;
  Object.assign(mockContext.UserStatsPerPool, {
    set: async (userStats: UserStatsPerPool) => { saved = userStats; },
  });
  return () => saved;
}

Usage in each test:

const getSaved = captureSet(mockContext);
// ... act ...
expect(getSaved()).toEqual(result);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Aggregators/Users.test.ts` around lines 50 - 68, Several tests repeat
Object.assign(mockContext.UserStatsPerPool, { set: ... }) and a savedUserStats
capture; extract that into a small helper (e.g., captureSet or captureUserStats)
placed inside the describe("updateUserStatsPerPool") scope that takes
mockContext (or mockContext.UserStatsPerPool), installs a set: async (userStats)
=> { saved = userStats; } and returns a getter function for the saved value;
then replace each test's Object.assign block with const getSaved =
captureSet(mockContext) and assert getSaved() equals the expected result;
reference the existing mockContext and the UserStatsPerPool.set method when
implementing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/EventHandlers/NFPM/NFPMIncreaseLiquidityLogic.test.ts`:
- Around line 358-404: Replace the manual ID template and non-deterministic Date
in the nonMatchingMintEvent fixture: use the CLPoolMintEventId(...) helper to
build the id (instead of the inline
`${chainId}_${poolAddress}_${transactionHash}_${increaseLogIndex - 1}`) and
replace createdAt: new Date() with a fixed timestamp (e.g., new Date(0) or the
shared baseTimestamp used elsewhere) in the nonMatchingMintEvent object so the
fixture matches other mint-event fixtures and is deterministic.

In `@test/EventHandlers/Pool/PoolBurnAndMintLogic.test.ts`:
- Line 70: Add assertions in the three processPoolLiquidityEvent tests to verify
that the mocked UserStatsPerPoolSnapshot.set was invoked through the
updateUserStatsPerPool → setUserStatsPerPoolSnapshot path: locate the tests that
call processPoolLiquidityEvent and after the invocation add expects against the
mock UserStatsPerPoolSnapshot.set (e.g., toHaveBeenCalled or
toHaveBeenCalledWith with the expected snapshot payload/keys), ensuring each
test asserts whether the snapshot was set or not according to that test's
conditions.

---

Nitpick comments:
In `@src/EventHandlers/CLPool.ts`:
- Around line 120-170: The CollectFees handler (CLPool.CollectFees.handler)
creates duplicate Date objects: it passes new Date(event.block.timestamp * 1000)
into loadOrCreateUserData and then constructs an identical timestamp later;
hoist a single const timestamp = new Date(event.block.timestamp * 1000) before
calling Promise.all so both loadOrCreateUserData and updateUserStatsPerPool use
the same timestamp instance, and apply the same change to the Flash and Swap
handlers (they also call loadOrCreateUserData with inline new Date(...) and
later redeclare timestamp); update CLPool.CollectFees.handler,
CLPool.Flash.handler, and CLPool.Swap.handler to compute timestamp once and
reuse it in both the concurrent loadOrCreateUserData call and the later
updateUserStatsPerPool/updateLiquidityPoolAggregator calls.

In `@src/EventHandlers/NFPM/NFPMDecreaseLiquidityLogic.ts`:
- Around line 75-81: The code creates a duplicate Date by calling new
Date(event.block.timestamp * 1000) before updateNonFungiblePosition even though
calculateDecreaseLiquidityDiff already computed and stored the timestamp in
nonFungiblePositionDiff.lastUpdatedTimestamp; change the call site to reuse that
existing Date (nonFungiblePositionDiff.lastUpdatedTimestamp) or adjust
calculateDecreaseLiquidityDiff to return the Date explicitly and then pass that
returned Date into updateNonFungiblePosition instead of constructing a new one
so both uses share the single Date object (references:
calculateDecreaseLiquidityDiff, nonFungiblePositionDiff.lastUpdatedTimestamp,
updateNonFungiblePosition).

In `@src/EventHandlers/Pool.ts`:
- Around line 90-138: Hoist the computed timestamp into a const before the
Promise.all so it’s created once and reused; specifically, compute const
timestamp = new Date(event.block.timestamp * 1000) above the Promise.all that
calls loadOrCreateUserData and then pass that timestamp into
loadOrCreateUserData (instead of creating a new Date inline) and reuse the same
timestamp when calling updateLiquidityPoolAggregator and updateUserStatsPerPool;
update the Pool.Fees handler (functions: loadOrCreateUserData,
updateLiquidityPoolAggregator, updateUserStatsPerPool) accordingly so the Date
allocation is not duplicated.

In `@src/EventHandlers/VotingReward/BribesVotingReward.ts`:
- Around line 52-57: The two places creating Date objects for the same timestamp
(the call to updateUserStatsPerPool and the earlier pool update around line 45)
should reuse a single Date instance for clarity; extract const timestamp = new
Date(data.timestamp * 1000) at the start of the handler and pass timestamp to
updateUserStatsPerPool (and to the pool update call) instead of creating new
Date(...) inline.

In `@src/Snapshots/LiquidityPoolAggregatorSnapshot.ts`:
- Around line 104-117: The parameter name for setLiquidityPoolAggregatorSnapshot
is inconsistent with other setters; rename the first parameter
liquidityPoolAggregator to entity to match conventions used in functions like
setVeNFTState, setNonFungiblePosition, setUserStatsPerPool, and setALMLPWrapper;
update all references within setLiquidityPoolAggregatorSnapshot (including the
call to createLiquidityPoolAggregatorSnapshot) and keep the rest of the function
logic unchanged so the SnapshotForPersist construction and persistSnapshot call
continue to work.

In `@test/Aggregators/NonFungiblePosition.test.ts`:
- Around line 112-117: Add a new unit test in NonFungiblePosition.test.ts that
seeds result.lastSnapshotTimestamp to getSnapshotEpoch(timestamp) (i.e., same
epoch as the incoming timestamp) and asserts that no new snapshot is taken: call
the same function/handler used in the other tests with that timestamp, then
verify result.lastSnapshotTimestamp remains unchanged and
mockContext.NonFungiblePositionSnapshot?.set was NOT called; use
getSnapshotEpoch(timestamp) to compute the pre-seeded epoch and reference the
same timestamp/result/mockContext variables used in the existing tests to keep
setup consistent.

In `@test/Aggregators/Users.test.ts`:
- Around line 50-68: Several tests repeat
Object.assign(mockContext.UserStatsPerPool, { set: ... }) and a savedUserStats
capture; extract that into a small helper (e.g., captureSet or captureUserStats)
placed inside the describe("updateUserStatsPerPool") scope that takes
mockContext (or mockContext.UserStatsPerPool), installs a set: async (userStats)
=> { saved = userStats; } and returns a getter function for the saved value;
then replace each test's Object.assign block with const getSaved =
captureSet(mockContext) and assert getSaved() equals the expected result;
reference the existing mockContext and the UserStatsPerPool.set method when
implementing.

In `@test/Aggregators/UserStatsPerPoolLiquidity.test.ts`:
- Around line 28-35: Replace the untracked no-op used for UserStatsPerPool.set
with a Jest mock so tests can assert calls; specifically, in the beforeEach
where mockContext is created via setupCommon(), change UserStatsPerPool: { set:
async () => {} } to use jest.fn() (e.g., UserStatsPerPool: { set: jest.fn() })
so the mockContext.UserStatsPerPool.set records invocations similar to
UserStatsPerPoolSnapshot.set.
- Around line 56-65: The tests call createMockUserStats() which yields an entity
with lastSnapshotTimestamp: undefined so shouldSnapshot(undefined,
mockTimestamp) returns true on the first updateUserStatsPerPool call and always
triggers UserStatsPerPoolSnapshot.set; update the other tests (besides "should
handle positive liquidity addition correctly") to assert snapshot behavior as
well—either assert UserStatsPerPoolSnapshot.set was called with the expected
snapshot object (using expect.objectContaining with userAddress, poolAddress,
chainId, etc.) or at minimum add a negative/positive call-count check like
expect(mockContext.UserStatsPerPoolSnapshot.set).not.toHaveBeenCalledTimes(0) to
reflect the first-call snapshot; update references to createMockUserStats,
shouldSnapshot, updateUserStatsPerPool, and UserStatsPerPoolSnapshot.set when
adding the assertions.

In `@test/Aggregators/VeNFTState.test.ts`:
- Around line 259-279: The first assertion that checks
updated.lastSnapshotTimestamp currently computes the epoch with a hardcoded
60*60*1000; change it to use getSnapshotEpoch(timestamp) (i.e., compare
updated.lastSnapshotTimestamp or its .getTime() to expectedEpoch or
expectedEpoch.getTime()) so both assertions use the same helper; remove the
redundant const expectedEpoch if you end up reusing the one declared later and
ensure the VeNFTStateSnapshot.set expectation continues to use
VeNFTStateSnapshotId, chainId, tokenId, owner, locktime, lastUpdatedTimestamp,
totalValueLocked, isAlive and timestamp as before.

In `@test/EventHandlers/Pool/PoolBurnAndMintLogic.test.ts`:
- Line 38: Replace the local construction of TIMESTAMP_DATE using new
Date(TIMESTAMP * 1000) with the canonical fixed timestamp/date provided by the
shared test helper (e.g., import and use the exported TIMESTAMP or
TIMESTAMP_DATE from common.ts), and pass that fixed value into
processPoolLiquidityEvent (reference: TIMESTAMP_DATE and
processPoolLiquidityEvent) so the test uses the shared, deterministic timestamp
fixture instead of constructing a Date inside the test.

In `@test/EventHandlers/VeNFT/VeNFTLogic.test.ts`:
- Around line 388-397: The tests repeatedly construct new
Date(mockTransferEvent.block.timestamp * 1000) inline; extract that into a
single named constant (e.g., const expectedTransferDate = new
Date(mockTransferEvent.block.timestamp * 1000)) and use that constant in all
assertions that call updateUserStatsSpy and any other assertions in the
updatePreviousOwnerUserStatsOnTransfer and updateNewOwnerUserStatsOnTransfer
describe blocks so the timestamp is fixed and reused across expect calls
involving updateUserStatsSpy, newOwnerStats, and related assertions.

In `@test/Snapshots/Shared.test.ts`:
- Around line 78-227: The tests in the "persistSnapshot" describe block
repeatedly call setupCommon(); extract that call into a beforeEach that assigns
the result to a local variable used by each test (e.g., declare let common; in
the describe and set common = setupCommon() inside beforeEach). Update each test
to remove the const common = setupCommon() lines and use the shared common
variable; keep all existing mocks and assertions referencing common, context,
and persistSnapshot as-is.

@FonsSicca FonsSicca force-pushed the dan/snapshot-implementation branch from ee5de64 to e611d1a Compare February 20, 2026 11:43
@FonsSicca FonsSicca merged commit 5994499 into main Feb 20, 2026
1 of 2 checks passed
@FonsSicca FonsSicca deleted the dan/snapshot-implementation branch February 20, 2026 11:46
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/EventHandlers/Pool.ts (1)

276-320: 🛠️ Refactor suggestion | 🟠 Major

Inconsistent timestamp handling in Pool.Claim compared to other handlers.

Other handlers (Fees, Swap) extract const timestamp = new Date(event.block.timestamp * 1000) at the top and reuse it for both loadOrCreateUserData and updateUserStatsPerPool. Here, loadOrCreateUserData receives an inline new Date(...) (line 290), and updateUserStatsPerPool receives result.poolDiff.lastUpdatedTimestamp as Date (line 308) — a different source.

Two concerns:

  1. The inline new Date(...) creates a distinct object from whatever processPoolClaim produces, breaking the single-source-of-truth pattern.
  2. The as Date assertion on line 308 masks a potential undefined if lastUpdatedTimestamp is optional in the diff type.

Consider aligning with the other handlers:

Proposed fix
 Pool.Claim.handler(async ({ event, context }) => {
+  const timestamp = new Date(event.block.timestamp * 1000);
   const [poolData, userData] = await Promise.all([
     loadPoolData(
       event.srcAddress,
       event.chainId,
       context,
       event.block.number,
       event.block.timestamp,
     ),
     loadOrCreateUserData(
       event.params.sender,
       event.srcAddress,
       event.chainId,
       context,
-      new Date(event.block.timestamp * 1000),
+      timestamp,
     ),
   ]);
 
   if (!poolData) {
     return;
   }
 
   const { liquidityPoolAggregator, token0Instance, token1Instance } = poolData;
 
   const result = processPoolClaim(
     event,
     event.params.sender,
     liquidityPoolAggregator.gaugeAddress ?? "",
     token0Instance,
     token1Instance,
   );
 
-  const timestamp = result.poolDiff.lastUpdatedTimestamp as Date;
-
   await Promise.all([
     updateLiquidityPoolAggregator(
       result.poolDiff,
       liquidityPoolAggregator,
       timestamp,
       context,
       event.chainId,
       event.block.number,
     ),
     updateUserStatsPerPool(result.userDiff, userData, context, timestamp),
   ]);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EventHandlers/Pool.ts` around lines 276 - 320, Compute a single timestamp
once at the top of Pool.Claim.handler (e.g., const timestamp = new
Date(event.block.timestamp * 1000)) and pass that same timestamp into
loadOrCreateUserData instead of constructing a new Date inline, and into
updateUserStatsPerPool instead of using result.poolDiff.lastUpdatedTimestamp as
Date; do not use the `as Date` assertion — either set
result.poolDiff.lastUpdatedTimestamp = timestamp after processPoolClaim or pass
timestamp directly to updateLiquidityPoolAggregator and updateUserStatsPerPool
so both use the same verified Date. Reference: Pool.Claim.handler,
loadOrCreateUserData, processPoolClaim, result.poolDiff.lastUpdatedTimestamp,
updateUserStatsPerPool, updateLiquidityPoolAggregator.
🧹 Nitpick comments (4)
src/EventHandlers/CLPool.ts (1)

236-249: Minor: duplicate new Date(...) in IncreaseObservationCardinalityNext.

Lines 238 and 244 each create a separate new Date(event.block.timestamp * 1000). While functionally equivalent, extracting a single const timestamp at the top (as the other handlers now do) would be more consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/EventHandlers/CLPool.ts` around lines 236 - 249, In
IncreaseObservationCardinalityNext, avoid creating duplicate Date objects by
extracting a single const timestamp = new Date(event.block.timestamp * 1000) at
the top of the handler and use that single timestamp when building
cardinalityDiff.lastUpdatedTimestamp and when calling
updateLiquidityPoolAggregator (replace both new Date(event.block.timestamp *
1000) occurrences); update references to use the timestamp variable alongside
existing symbols cardinalityDiff and updateLiquidityPoolAggregator.
test/Snapshots/Shared.test.ts (1)

78-227: Good coverage of all persistSnapshot branches.

Each snapshot type is verified, plus the exhaustiveness guard. Tests correctly use setupCommon() builders and fixed timestamps.

Consider test.each to reduce the boilerplate across the six per-type tests — each follows an identical arrange/act/assert shape differing only in the type, entity builder, and context key. As per coding guidelines: "Use test.each for table-driven tests with scenario matrices."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Snapshots/Shared.test.ts` around lines 78 - 227, The tests duplicate the
same arrange/act/assert for each SnapshotType; replace the six per-type it
blocks with a single table-driven test using test.each that iterates rows of {
type: SnapshotType.X, contextKey: "XSnapshot", entityBuilder: common.createMockX
/ data, snapshotCreator: createXSnapshot, timestamp: new Date(...) }, call
persistSnapshot({ type, snapshot }, context) inside the test, and assert
context[contextKey].set was called once with snapshot; keep the existing
exhaustiveness test as-is. Reference persistSnapshot, SnapshotType, the various
create*Snapshot helpers (createLiquidityPoolAggregatorSnapshot,
createUserStatsPerPoolSnapshot, createNonFungiblePositionSnapshot,
createALMLPWrapperSnapshot, createVeNFTStateSnapshot, createTokenPriceSnapshot)
and the context keys (LiquidityPoolAggregatorSnapshot, UserStatsPerPoolSnapshot,
NonFungiblePositionSnapshot, ALM_LP_WrapperSnapshot, VeNFTStateSnapshot,
TokenPriceSnapshot) when implementing the table and assertions.
src/Snapshots/TokenPriceSnapshot.ts (1)

20-37: Consider an options object to reduce positional parameter fragility.

Both createTokenPriceSnapshot and setTokenPriceSnapshot accept 6–7 positional parameters, two of which (chainId and blockNumber) share the same type (number). This makes call sites prone to silent argument-swap bugs. An options/params object would add clarity and safety.

♻️ Sketch
+interface TokenPriceSnapshotParams {
+  address: string;
+  chainId: number;
+  blockNumber: number;
+  lastUpdatedTimestamp: Date;
+  pricePerUSDNew: bigint;
+  isWhitelisted: boolean;
+}
+
-export function createTokenPriceSnapshot(
-  address: string,
-  chainId: number,
-  blockNumber: number,
-  lastUpdatedTimestamp: Date,
-  pricePerUSDNew: bigint,
-  isWhitelisted: boolean,
-): TokenPriceSnapshot {
+export function createTokenPriceSnapshot(
+  params: TokenPriceSnapshotParams,
+): TokenPriceSnapshot {
+  const { address, chainId, blockNumber, lastUpdatedTimestamp, pricePerUSDNew, isWhitelisted } = params;
   const snapshotId = TokenIdByBlock(chainId, address, blockNumber);
   ...
 }

Also applies to: 50-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Snapshots/TokenPriceSnapshot.ts` around lines 20 - 37, The factory
createTokenPriceSnapshot (and similarly setTokenPriceSnapshot) is fragile due to
multiple same-typed positional params (chainId, blockNumber); change the
signature to accept a single options object (e.g. { address, chainId,
blockNumber, lastUpdatedTimestamp, pricePerUSDNew, isWhitelisted }) and update
the function body to destructure that object and still call
TokenIdByBlock(chainId, address, blockNumber) to produce the id; then update all
call sites to pass a single options object instead of positional args to prevent
silent argument-swaps while preserving the TokenPriceSnapshot return shape.
test/Snapshots/VeNFTStateSnapshot.test.ts (1)

54-95: Minor overlap between the two setVeNFTStateSnapshot epoch-alignment tests.

Lines 54–73 ("should compute snapshot epoch correctly") and lines 75–95 ("should set snapshot with epoch-aligned timestamp and correct id") both exercise setVeNFTStateSnapshot and assert epoch-aligned id + timestamp. They differ only in which epoch boundary they target. Consider consolidating into a single test.each with multiple timestamp/epoch pairs, or dropping one to reduce duplication.

♻️ Example consolidation with test.each
-  it("should compute snapshot epoch correctly (floor timestamp to interval boundary)", () => {
-    ...
-    const midEpochTimestamp = new Date(
-      SNAPSHOT_INTERVAL_IN_MS * 3 + 25 * 60 * 1000,
-    );
-    const expectedEpochMs = SNAPSHOT_INTERVAL_IN_MS * 3;
-    ...
-  });
-
-  it("should set snapshot with epoch-aligned timestamp and correct id", () => {
-    ...
-    const timestamp = new Date(baseTimestamp.getTime() + 10 * 60 * 1000);
-    ...
-    const expectedEpochMs = SNAPSHOT_INTERVAL_IN_MS * 2;
-    ...
-  });
+  it.each([
+    { label: "mid-epoch (25min into 3rd hour)", timestampMs: SNAPSHOT_INTERVAL_IN_MS * 3 + 25 * 60 * 1000, expectedEpochMs: SNAPSHOT_INTERVAL_IN_MS * 3 },
+    { label: "10min past baseTimestamp",        timestampMs: SNAPSHOT_INTERVAL_IN_MS * 2 + 10 * 60 * 1000, expectedEpochMs: SNAPSHOT_INTERVAL_IN_MS * 2 },
+  ])("should set epoch-aligned snapshot ($label)", ({ timestampMs, expectedEpochMs }) => {
+    const context = common.createMockContext({ VeNFTStateSnapshot: { set: jest.fn() } });
+    const entity = common.createMockVeNFTState();
+    setVeNFTStateSnapshot(entity, new Date(timestampMs), context);
+    const setArg = (context.VeNFTStateSnapshot.set as jest.Mock).mock.calls[0][0];
+    expect(setArg.timestamp.getTime()).toBe(expectedEpochMs);
+    expect(setArg.id).toBe(VeNFTStateSnapshotId(entity.chainId, entity.tokenId, expectedEpochMs));
+  });

As per coding guidelines: "Use test.each for table-driven tests with scenario matrices".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Snapshots/VeNFTStateSnapshot.test.ts` around lines 54 - 95, The two
tests both exercise setVeNFTStateSnapshot and assert epoch-aligned id/timestamp,
causing duplication; refactor them into a single table-driven test using
test.each that supplies multiple {name, inputTimestamp, expectedEpochMs,
entityOverrides} rows (e.g., one row for the 3rd-hour midEpochTimestamp case and
one for the baseTimestamp+10min case), and inside the test call
setVeNFTStateSnapshot(entity, inputTimestamp, context) and assert the same
id/timestamp expectations using VeNFTStateSnapshotId and
SNAPSHOT_INTERVAL_IN_MS; keep references to setVeNFTStateSnapshot,
VeNFTStateSnapshotId, SNAPSHOT_INTERVAL_IN_MS and the mock context so the
assertions remain identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/EventHandlers/Pool.ts`:
- Around line 276-320: Compute a single timestamp once at the top of
Pool.Claim.handler (e.g., const timestamp = new Date(event.block.timestamp *
1000)) and pass that same timestamp into loadOrCreateUserData instead of
constructing a new Date inline, and into updateUserStatsPerPool instead of using
result.poolDiff.lastUpdatedTimestamp as Date; do not use the `as Date` assertion
— either set result.poolDiff.lastUpdatedTimestamp = timestamp after
processPoolClaim or pass timestamp directly to updateLiquidityPoolAggregator and
updateUserStatsPerPool so both use the same verified Date. Reference:
Pool.Claim.handler, loadOrCreateUserData, processPoolClaim,
result.poolDiff.lastUpdatedTimestamp, updateUserStatsPerPool,
updateLiquidityPoolAggregator.

---

Nitpick comments:
In `@src/EventHandlers/CLPool.ts`:
- Around line 236-249: In IncreaseObservationCardinalityNext, avoid creating
duplicate Date objects by extracting a single const timestamp = new
Date(event.block.timestamp * 1000) at the top of the handler and use that single
timestamp when building cardinalityDiff.lastUpdatedTimestamp and when calling
updateLiquidityPoolAggregator (replace both new Date(event.block.timestamp *
1000) occurrences); update references to use the timestamp variable alongside
existing symbols cardinalityDiff and updateLiquidityPoolAggregator.

In `@src/Snapshots/TokenPriceSnapshot.ts`:
- Around line 20-37: The factory createTokenPriceSnapshot (and similarly
setTokenPriceSnapshot) is fragile due to multiple same-typed positional params
(chainId, blockNumber); change the signature to accept a single options object
(e.g. { address, chainId, blockNumber, lastUpdatedTimestamp, pricePerUSDNew,
isWhitelisted }) and update the function body to destructure that object and
still call TokenIdByBlock(chainId, address, blockNumber) to produce the id; then
update all call sites to pass a single options object instead of positional args
to prevent silent argument-swaps while preserving the TokenPriceSnapshot return
shape.

In `@test/Snapshots/Shared.test.ts`:
- Around line 78-227: The tests duplicate the same arrange/act/assert for each
SnapshotType; replace the six per-type it blocks with a single table-driven test
using test.each that iterates rows of { type: SnapshotType.X, contextKey:
"XSnapshot", entityBuilder: common.createMockX / data, snapshotCreator:
createXSnapshot, timestamp: new Date(...) }, call persistSnapshot({ type,
snapshot }, context) inside the test, and assert context[contextKey].set was
called once with snapshot; keep the existing exhaustiveness test as-is.
Reference persistSnapshot, SnapshotType, the various create*Snapshot helpers
(createLiquidityPoolAggregatorSnapshot, createUserStatsPerPoolSnapshot,
createNonFungiblePositionSnapshot, createALMLPWrapperSnapshot,
createVeNFTStateSnapshot, createTokenPriceSnapshot) and the context keys
(LiquidityPoolAggregatorSnapshot, UserStatsPerPoolSnapshot,
NonFungiblePositionSnapshot, ALM_LP_WrapperSnapshot, VeNFTStateSnapshot,
TokenPriceSnapshot) when implementing the table and assertions.

In `@test/Snapshots/VeNFTStateSnapshot.test.ts`:
- Around line 54-95: The two tests both exercise setVeNFTStateSnapshot and
assert epoch-aligned id/timestamp, causing duplication; refactor them into a
single table-driven test using test.each that supplies multiple {name,
inputTimestamp, expectedEpochMs, entityOverrides} rows (e.g., one row for the
3rd-hour midEpochTimestamp case and one for the baseTimestamp+10min case), and
inside the test call setVeNFTStateSnapshot(entity, inputTimestamp, context) and
assert the same id/timestamp expectations using VeNFTStateSnapshotId and
SNAPSHOT_INTERVAL_IN_MS; keep references to setVeNFTStateSnapshot,
VeNFTStateSnapshotId, SNAPSHOT_INTERVAL_IN_MS and the mock context so the
assertions remain identical.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Token holdings accounting in user snapshots - CL and non CL Pools Entities snapshots - Possible use case of block handlers feature from envio

2 participants