Skip to content

fix: get 7d and 1d volume from pools, both in v2 and v3#2572

Open
gustavobftorres wants to merge 16 commits into
DefiLlama:masterfrom
gustavobftorres:fix/balancer-pools-volume
Open

fix: get 7d and 1d volume from pools, both in v2 and v3#2572
gustavobftorres wants to merge 16 commits into
DefiLlama:masterfrom
gustavobftorres:fix/balancer-pools-volume

Conversation

@gustavobftorres

@gustavobftorres gustavobftorres commented Apr 10, 2026

Copy link
Copy Markdown

Add Balancer V2/V3 volumeUsd1d and volumeUsd7d to Yields adapters

Summary

This PR fills missing 1d/7d volume fields for Balancer pools in DefiLlama Yields so show7dVolume=true displays values for Balancer V2 and Balancer V3 pools.

What changed

1) Balancer V2 adapter (src/adaptors/balancer-v2/index.js)

  • Extended subgraph queries to include cumulative totalSwapVolume (current and prior-block queries).
  • Added 7-day prior block fetch using utils.getBlocks(..., 604800).
  • Computed per-pool:
    • volumeUsd1d = totalSwapVolume(now) - totalSwapVolume(1dAgo)
    • volumeUsd7d = totalSwapVolume(now) - totalSwapVolume(7dAgo)
  • Added clamping for invalid/negative deltas:
    • if delta is not finite or <= 0, value is set to 0.
  • Returned volumeUsd1d and volumeUsd7d in adapter pool output.

2) Balancer V3 adapter (src/adaptors/balancer-v3/index.ts)

  • Kept existing poolGetPools flow for TVL/APY data.
  • Added dynamicData.volume24h to base pool query.
  • Added snapshot-based 7d logic via Balancer API poolGetSnapshots with range: THIRTY_DAYS.
  • Implemented batched snapshot fetching with GraphQL aliases (SNAPSHOT_BATCH_SIZE = 25) to reduce request count.
  • For each pool:
    • Sort snapshots by timestamp descending.
    • volumeUsd7d = sum(volume24h of latest up to 7 snapshots) (supports partial history for newer pools).
    • volumeUsd1d = dynamicData.volume24h with fallback to latest snapshot day.
  • Returned volumeUsd1d and volumeUsd7d in adapter pool output.

Why

Balancer V2/V3 pools were missing volumeUsd7d in Yields responses, causing blank 7d volume UI fields.
These changes populate the existing optional fields already supported by yield-server ingestion/query layers.

Compatibility

  • No DB schema migration required.
  • No API contract changes.
  • Only enriches existing optional fields for Balancer V2/V3 pool records.

Notes

  • Snapshot range remains THIRTY_DAYS (minimum available in Balancer API), with local slicing to 7 days.
  • Negative/invalid V2 deltas are clamped to zero to avoid null/incorrect UI behavior during transient data inconsistencies.

Summary by CodeRabbit

  • New Features
    • Balancer V2/V3 pools now include 1‑day and 7‑day swap volume; V3 adds snapshot-based volume computation with batched snapshot fetching and retries.
  • Bug Fixes
    • Subgraph failures now fall back to API data; prior-block gaps handled; token price and reward APR failures default safely to zero.
  • Tests
    • Tests require at least one pool and more robustly handle missing fields.
  • Chores
    • CI test workflow tightened; safer adapter startup handling and improved per-chain job logging.

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds snapshot-based volume computation and a Balancer API fallback when subgraph queries fail, batches snapshot requests, hardens error handling around price/LM APR and tests, and refactors per-chain job execution with added per-chain rejection logging. Returned pool objects now include volumeUsd1d and volumeUsd7d.

Changes

Cohort / File(s) Summary
Balancer V2 adaptor
src/adaptors/balancer-v2/index.js
Adds fallback path using Balancer API when current/prior subgraph queries fail or return empty; batches per-pool snapshot requests (chunk size 25); computes volumeUsd1d/volumeUsd7d from snapshots; wraps prior-block queries and price/LM APR lookups with try/catch and sensible defaults; refactors chain execution to a chainJobs list with per-chain rejection logging.
Balancer V3 adaptor (TS)
src/adaptors/balancer-v3/index.ts
Extends pool queries to include dynamicData.volume24h; implements batched snapshot fetching across Balancer API URLs with aliased queries; helpers for numeric coercion, chunking, query building, and snapshot-to-volume aggregation; prefers dynamicData.volume24h when finite, otherwise falls back to snapshot-derived volumeUsd1d and conditionally attaches volumeUsd7d; marks snapshot batches unavailable on failure and skips malformed pools.
Test harness defensiveness
src/adaptors/beforeTests.js
Guards against non-array apy returns (apyRaw), only sorts when array; derives adapterProject safely; tightens global.uniquePoolIdentifiersDB filtering to skip malformed records and only exclude by project when adapterProject exists.
Adapter tests
src/adaptors/test.js
Adds assertion that adapter apy array has at least one pool; uses optional chaining for apy[0] when validating pool URL; derives firstProject defensively before checking against known protocol slugs.
CI workflow
.github/workflows/test.yml
Simplifies test loop by removing the subshell/`

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant TopLvl as TopLvl
  participant Subgraph as Subgraph
  participant BalancerAPI as Balancer API (Snapshots)
  participant Pools as PoolsProcessor
  participant PriceSvc as TokenPriceService

  TopLvl->>Subgraph: fetch current pools (+prior/prior7d)
  alt Subgraph returns valid pools
    Subgraph-->>TopLvl: pools (+dynamicData.volume24h)
    TopLvl->>Pools: map pools, attach dynamic volumes when finite
  else Subgraph fails or empty
    Subgraph-->>TopLvl: error/empty
    TopLvl->>BalancerAPI: fetch pool list + snapshots (batched)
    BalancerAPI-->>TopLvl: pool list + snapshots (or unavailable sentinel)
    TopLvl->>Pools: build pools using snapshot-derived volumeUsd1d/7d
  end
  Pools->>PriceSvc: fetch token prices & LM APRs (try/catch)
  PriceSvc-->>Pools: prices / aprs (or defaults on error)
  Pools-->>TopLvl: enriched pool objects (include volumeUsd1d, volumeUsd7d, aprLM, rewardTokens)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

Hop, hop—I chased snapshots through the night,
Batches lined up till the volumes looked right,
One-day and seven tucked into each row,
V2 and V3 hum while the numbers grow,
A rabbit cheers as the data takes flight 🐰✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding 1-day and 7-day volume fields for both Balancer V2 and V3 pools, which is the core objective across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/adaptors/balancer-v2/index.js`:
- Around line 428-431: The volumeDelta function currently turns an absent prior
value into NaN (via Number(undefined)) causing new pools to show 0; update
volumeDelta (function name: volumeDelta) to treat a missing prior as 0 before
computing the difference (e.g., coerce volumePrior undefined/null to 0), then
compute delta = Number(volumeNow) - Number(coercedPrior) and finally clamp to
non-negative only on the final delta (using Number.isFinite check and delta >
0). This preserves cumulative volume when the historical row is missing while
still preventing negative or non-finite results.

In `@src/adaptors/balancer-v3/index.ts`:
- Around line 73-92: The current catch in the batch loop swallows errors from
request(BALANCER_API_URL, snapshotsQuery, ...) and then seeds every pool in that
batch with an empty array via snapshotsByPoolId, which yields incorrect zero
volumes; either rethrow the error to let the outer chain-level handler drop the
chain, or record a clear "volume unavailable" sentinel per pool instead of [] so
downstream code (which computes volumeUsd7d) can detect unavailable data. Update
the try/catch around request in buildSnapshotsQuery usage: on error either throw
the caught error (preserving stack) or set
snapshotsByPoolId.set(poolId.toLowerCase(), VOLUME_UNAVAILABLE_SENTINEL) for
each pool in the batch (define a unique sentinel such as null or an object like
{unavailable:true}) and adjust downstream checks to treat that sentinel as
unavailable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85c20818-d657-4efd-8c5e-222e90a68bf1

📥 Commits

Reviewing files that changed from the base of the PR and between 048d775 and 44c75a9.

📒 Files selected for processing (2)
  • src/adaptors/balancer-v2/index.js
  • src/adaptors/balancer-v3/index.ts

Comment thread src/adaptors/balancer-v2/index.js Outdated
Comment thread src/adaptors/balancer-v3/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/adaptors/beforeTests.js (1)

58-59: Fail fast when apy() returns the wrong shape.

Coercing non-array results to [] turns a broken adapter contract into downstream “0 pools / missing link / empty project” noise. Throw here with the adapter name so CI surfaces the real failure at setup time.

♻️ Proposed fix
   global.adapter = adapter;
   const apyRaw = await module.apy(timestamp);
-  global.apy = (Array.isArray(apyRaw) ? apyRaw : []).sort(
+  if (!Array.isArray(apyRaw)) {
+    throw new TypeError(`${adapter}.apy() must return an array`);
+  }
+  global.apy = apyRaw.sort(
     (a, b) => b.tvlUsd - a.tvlUsd
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adaptors/beforeTests.js` around lines 58 - 59, The code currently coerces
non-array apy() results to [] which hides adapter contract failures; update the
setup to validate that apyRaw from module.apy(timestamp) is an Array and if not
immediately throw an Error that includes the adapter identifier (e.g.,
module.name or module.adapterName) and the received type/value; replace the
current coercion in the global.apy assignment so you only assign sorted apy when
Array.isArray(apyRaw) is true, otherwise throw with a clear message referencing
apyRaw, module.apy and the adapter name so CI fails fast.
src/adaptors/test.js (1)

69-72: Short-circuit the first-pool checks after the Line 26 emptiness assertion.

A zero-pool adapter now fails three times in the same suite (length, url, and project), which makes CI noisier than it needs to be. Let the explicit emptiness test own that failure and skip these two first-pool-dependent assertions when apy.length === 0.

♻️ Proposed fix
   test("Check if link to the pool's page exist", () => {
+    if (apy.length === 0) return;
     const poolsLink = apy[0]?.url || poolsUrl;
     expect(typeof poolsLink).toBe('string');
   });
@@
   test('All pools should have the same project field matching the adapter name and a known protocol slug', () => {
+    if (apy.length === 0) return;
     const projectNames = [...new Set(apy.map((p) => p.project))];
     expect(projectNames).toEqual([adapter]);
     const firstProject = apy[0]?.project;
     expect(typeof firstProject).toBe('string');
     expect(protocols).toContain(firstProject);

Also applies to: 182-188

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

In `@src/adaptors/test.js` around lines 69 - 72, The tests that inspect the first
pool (the "Check if link to the pool's page exist" test using apy, poolsLink,
poolsUrl) should early-return when the collection is empty: after the explicit
emptiness assertion for apy (the test that verifies apy.length === 0), add a
guard in the dependent tests so they skip when apy.length === 0 (e.g., if
(apy.length === 0) return); apply the same short-circuit to the other
first-pool-dependent tests later in the file (the block around the project/url
checks at the second occurrence) so only the emptiness test fails for zero-pool
adapters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/adaptors/beforeTests.js`:
- Around line 58-59: The code currently coerces non-array apy() results to []
which hides adapter contract failures; update the setup to validate that apyRaw
from module.apy(timestamp) is an Array and if not immediately throw an Error
that includes the adapter identifier (e.g., module.name or module.adapterName)
and the received type/value; replace the current coercion in the global.apy
assignment so you only assign sorted apy when Array.isArray(apyRaw) is true,
otherwise throw with a clear message referencing apyRaw, module.apy and the
adapter name so CI fails fast.

In `@src/adaptors/test.js`:
- Around line 69-72: The tests that inspect the first pool (the "Check if link
to the pool's page exist" test using apy, poolsLink, poolsUrl) should
early-return when the collection is empty: after the explicit emptiness
assertion for apy (the test that verifies apy.length === 0), add a guard in the
dependent tests so they skip when apy.length === 0 (e.g., if (apy.length === 0)
return); apply the same short-circuit to the other first-pool-dependent tests
later in the file (the block around the project/url checks at the second
occurrence) so only the emptiness test fails for zero-pool adapters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8064f90a-ba81-480f-a012-4f88c6999833

📥 Commits

Reviewing files that changed from the base of the PR and between 44c75a9 and 5b14cb8.

📒 Files selected for processing (3)
  • .github/workflows/test.yml
  • src/adaptors/beforeTests.js
  • src/adaptors/test.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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/adaptors/balancer-v3/index.ts (1)

167-184: ⚠️ Potential issue | 🟠 Major

Don't let null volume24h suppress the snapshot fallback.

Line 167 turns null into 0, and Lines 181-184 treat that as authoritative, so volumeUsd1d can be reported as 0 even when the latest snapshot has non-zero volume. This also diverges from the V2 path in src/adaptors/balancer-v2/index.js:574-581, which only trusts dynamic 24h volume when it is positive.

Proposed fix
-      const dynamicVolume24h = Number(pool.dynamicData?.volume24h);
+      const rawDynamicVolume24h = pool.dynamicData?.volume24h;
+      const dynamicVolume24h =
+        rawDynamicVolume24h == null ? undefined : Number(rawDynamicVolume24h);
       const poolData = {
         pool: pool.address,
         chain: utils.formatChain(chainString),
         project: 'balancer-v3',
         symbol: utils.formatSymbol(pool.symbol),
@@
-      if (Number.isFinite(dynamicVolume24h)) {
+      if (
+        dynamicVolume24h !== undefined &&
+        Number.isFinite(dynamicVolume24h) &&
+        dynamicVolume24h > 0
+      ) {
         poolData.volumeUsd1d = dynamicVolume24h;
       } else if (volumeUsd1dFromSnapshots !== undefined) {
         poolData.volumeUsd1d = volumeUsd1dFromSnapshots;
+      } else if (
+        dynamicVolume24h !== undefined &&
+        Number.isFinite(dynamicVolume24h)
+      ) {
+        poolData.volumeUsd1d = dynamicVolume24h;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adaptors/balancer-v3/index.ts` around lines 167 - 184, The code converts
pool.dynamicData?.volume24h to a Number (dynamicVolume24h) which turns null into
0, and then the if (Number.isFinite(dynamicVolume24h)) branch treats 0 as
authoritative; change the logic so you only trust dynamic 24h volume when the
raw value is present and positive. Inspect pool.dynamicData?.volume24h (or a
rawVolume variable) and only set poolData.volumeUsd1d from dynamicVolume24h when
rawVolume != null/undefined and Number(rawVolume) > 0; otherwise fall back to
volumeUsd1dFromSnapshots as before (references: pool.dynamicData?.volume24h,
dynamicVolume24h, poolData.volumeUsd1d, volumeUsd1dFromSnapshots).
🤖 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/adaptors/balancer-v3/index.ts`:
- Around line 167-184: The code converts pool.dynamicData?.volume24h to a Number
(dynamicVolume24h) which turns null into 0, and then the if
(Number.isFinite(dynamicVolume24h)) branch treats 0 as authoritative; change the
logic so you only trust dynamic 24h volume when the raw value is present and
positive. Inspect pool.dynamicData?.volume24h (or a rawVolume variable) and only
set poolData.volumeUsd1d from dynamicVolume24h when rawVolume != null/undefined
and Number(rawVolume) > 0; otherwise fall back to volumeUsd1dFromSnapshots as
before (references: pool.dynamicData?.volume24h, dynamicVolume24h,
poolData.volumeUsd1d, volumeUsd1dFromSnapshots).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 898573a9-193d-41eb-8c5d-66c2040c5e02

📥 Commits

Reviewing files that changed from the base of the PR and between 5dc50f9 and 875aaeb.

📒 Files selected for processing (1)
  • src/adaptors/balancer-v3/index.ts

@0xkr3p

0xkr3p commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

hi @gustavobftorres, thank you for your changes, please separate CI / test changes into separate PR

@github-actions

Copy link
Copy Markdown

The balancer-v2 adapter exports pools:

Test Suites: 1 passed, 1 total
Tests: 32500 passed, 32500 total
Snapshots: 0 total
Time: 5.412 s
Ran all test suites.

return pools
.filter((pool) => pool.id || pool.address)
.map((pool) => {
const poolId = pool.id || pool.address;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pls remove the pool.address fallback. The pool field should only have one value else historical data can be lost as a new pool value = new entry

// balancer splits off a pct cut of swap fees to the protocol, get pct value:
const swapFeePercentage =
(
let swapFeePercentage = 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

onchain value is 0.5, should this be the default rather than 1?

token: tokenAddress,
tvlUsd: toNumber(pool.dynamicData?.totalLiquidity),
apyBase: 0,
apyReward: 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

setting apyBase/Reward to 0 is misleading, they may not be 0. I would opt to drop pool than set to 0 due to incomplete data

chain: utils.formatChain(chainString),
project: 'balancer-v3',
symbol: utils.formatSymbol(pool.symbol || ''),
tvlUsd: toNumber(dynamicData.totalLiquidity),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Previously Number(pool.dynamicData.totalLiquidity) would emit NaN for
malformed values and utils.keepFinite filtered the pool out. Now toNumber(...) coerces NaN to 0,
and the same pattern on apyBase/apyReward (lines 339–340) does likewise. Pools that previously
got dropped will now appear with tvlUsd: 0 / apyBase: 0 — same problem as the V2 fallback. Either
keep the NaN-flows-through behavior, or pair these coercions with a guard that drops pools where the core fields all coerced to 0

@0xkr3p 0xkr3p left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hey @gustavobftorres, thanks for the PR, just a few comments to resolve pls

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.

2 participants