Skip to content

feat: holders and delegates v3 (DEV-562, DEV-476) - #2084

Open
brunod-e wants to merge 25 commits into
devfrom
feat/holders-delegates-v3
Open

feat: holders and delegates v3 (DEV-562, DEV-476)#2084
brunod-e wants to merge 25 commits into
devfrom
feat/holders-delegates-v3

Conversation

@brunod-e

@brunod-e brunod-e commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What

Holders & Delegates v3. Closes DEV-562 and DEV-476.

Dashboard

  • Min/max value filters on the Delegates (voting power) and Token Holders (balance) tables (DEV-476).
  • Delegates is the default tab; sidebar and page title read "Stakeholders".
  • Main table: default row height, row bottom borders (new withRowBorders on the design-system Table), continuous activity ring, voting power as a percentage of quorum (red at 100%+, yellow at 50%+).
  • Inactive-delegate flag and 0/0 activity states (Inactive / No proposals / Never voted), respecting the selected period.
  • Inactive-VP alert banner on Token Holders, using the design-system InlineAlert; "Voted X/Y (Inactive)" under the delegate name.
  • Clickable addresses that re-point the drawer everywhere; per-address Activity tab in the drawer.
  • Balance History: Buy/Sell relabeled to In / Out / Vesting (vesting resolved through the labels endpoint).
  • Top Interactions: Dust badge (< $1 at spot price) and a "Hide dust" switch (new design-system Switch); Total Interactions shown as a USD value.
  • Voting Power History: "Filter low importance" (default on), sitting on the CSV row.
  • Time selector: MAX option and a custom calendar range (new SwitcherDateRange); "Max available data" replaces "All time".
  • Delegate drawer tabs aligned to Figma (Voting Power, Delegation History); Former Delegators view with the VP-impact transition; both variation charts show their net change value in the header.
  • Design-system SegmentedControl for the current/former delegators view and for the Activity feed filters.

API

  • New endpoints: GET /:dao/voting-powers/inactive-summary, GET /:dao/accounts/:address/delegators/historical, GET /:dao/addresses/labels.
  • GET /:dao/feed/events gains an optional address filter (matches the address in any role, case-insensitive) and accepts relevance=ALL, which drops the value floor. Omitting relevance still defaults to MEDIUM, so existing consumers are unaffected.
  • Gateful re-exposes the expanded surface through its aggregated OpenAPI spec (no gateway code change).

Testing

  • API: typecheck and lint clean, 1045 unit tests pass.
  • Dashboard: typecheck and lint clean, 225 tests pass.
  • Verified the address filter end to end against a local API + Gateful with real ENS data: unfiltered totalCount 8729 versus 1980 for a single address, and every returned event involves that address.
  • Measured the delegate cell alignment, the table row borders and the long-name truncation in the rendered page rather than eyeballing them, including a box-model repro for the truncation chain.

Self-review

Ran a multi-dimension review over the whole diff (API logic, API contract, data fetching, dashboard computation, design system and accessibility, tests, conventions) with an adversarial pass that tried to refute each finding. What survived and got fixed in this PR:

  • Unbounded retry loop. useDelegatesActivity only recorded addresses whose fetch succeeded. A rejected or empty response left the address selectable again the moment it left the loading set, and since the effect depends on both sets it refetched the same addresses forever while the endpoint kept failing. Fetches now settle into their own set, and one address failing no longer discards the others.
  • AAVE. Its API registers no proposal endpoints, so the shared Token Holders table fired a 404 per delegate for proposals-activity and a 400 for the inactive summary, which falls through to the /voting-powers/{address} param route. Both are now gated on the DAO exposing proposal activity.
  • Address enrichment refetch storm. useGetAddress/useGetAddresses had no stale time while the global QueryClient defaults to 0, so every avatar refetched on remount. Opening a tab fired hundreds of requests, many cancelled and refired. Now cached for 5 minutes.
  • Unstable cache key. activityFromDate was recomputed from Date.now() on every render, so it changed whenever a render crossed a second boundary and re-keyed both the activity cache and the banner query.
  • Total VP Lost summed only the pages already loaded while printing the API's count of every former delegator beside it. It now claims a total only once there is nothing left to load.
  • Inactive summary window now excludes canceled proposals, so it agrees with proposals-activity instead of flagging a delegate as inactive for skipping a vote that never happened.
  • Failed requests in the drawer Activity feed and the Former Delegators table no longer render as "nothing found"; the USD column shows a dash instead of a confident "$0" while the token price is in flight; and an address inside DrawerAddressButton no longer nests the tooltip trigger button inside the row button, which cost a second tab stop and handed the accessible name to the tooltip.

Removed from scope

The proposal Final Result filter (and the proposalStatusIn param that backed it) was pulled out of this PR. The filter matched the stored proposals_onchain.status, but the service overwrites each proposal's status at read time with a derived value, and the indexer only ever persists ACTIVE, CANCELED, EXECUTED, PENDING, QUEUED and VETOED. So DEFEATED, SUCCEEDED, NO_QUORUM, EXPIRED and PENDING_EXECUTION could never match: on ENS, "Failed" returned zero rows and zeroed all four metric cards, "Passed" silently dropped SUCCEEDED, and "Canceled" was unsatisfiable because the query already excludes canceled proposals. Making it correct means either persisting the derived status from the indexer or expressing the derivation in SQL over end block, vote tallies, quorum and timelock, which is its own task rather than a detail of this one.

Notes for review

  • The @anticapture/client SDK regenerates on the preview from the deployed Gateful spec, so the features backed by the new endpoints (inactive banner, Former Delegators, per-address Activity, Vesting labels) only show live data once this branch's API and Gateful are deployed. Local typechecks can pass or fail depending on which Gateful the codegen reached, so code should not compare against generated enum values it cannot guarantee exist.
  • Known limitations, worth a follow-up rather than a blocker: the dust filter on Top Interactions runs client-side after server pagination, so a page whose rows are all dust renders empty; toDate reaches the voting-powers query and the banner but not the per-delegate activity request, so activity is computed over fromDate..now; and the new design-system Switch still hardcodes two colors instead of semantic tokens and ships without a story.
  • Open design decisions kept as-is: the min/max filter lives in the funnel on the Voting Power / Balance headers (Figma shows only sort arrows there), and the shared drawer header uses the default EnsAvatar tag set rather than Figma's Individual/EAO badges.

🤖 Generated with Claude Code

Dashboard:
- min/max value filters on Delegates (voting power) and Token Holders (balance)
- Delegates as default tab; sidebar renamed to "Stakeholders"
- larger rows with bottom borders, continuous activity ring, VP as percent of quorum
- inactive-delegate flag and 0/0 states (Inactive / No proposals / Never voted)
- inactive-VP alert banner on Token Holders; "Voted X/Y (Inactive)" on delegate column
- clickable addresses that re-point the drawer; per-address Activity tab
- Balance History In / Out / Vesting; dust badge and Hide dust switch on Top Interactions
- VP History low-importance filter and All time; time selector MAX plus custom calendar range
- delegate drawer tabs renamed (Voting Power, Delegation History); Former Delegators view
- proposal final-result filter on the votes tab

API:
- new endpoints: voting-powers/inactive-summary, accounts/:address/delegators/historical,
  addresses/labels; address filter on feed/events; proposalStatusIn on proposals-activity

Co-Authored-By: Claude Fable 5 <[email protected]>
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
anticapture-storybook Ready Ready Preview Jul 30, 2026 1:10am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
anticapture Ignored Ignored Jul 30, 2026 1:10am

Request Review

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🔍 Vercel preview: https://anticapture-46kslcqjk-ful.vercel.app

@railway-app

railway-app Bot commented Jul 23, 2026

Copy link
Copy Markdown

🚅 Deployed to the anticapture-pr-2084 environment in anticapture-infra

Service Status Web Updated (UTC)
shutter-api ✅ Success (View Logs) Jul 30, 2026 at 1:10 am
uniswap-api ✅ Success (View Logs) Jul 30, 2026 at 1:10 am
nouns-api ✅ Success (View Logs) Jul 30, 2026 at 1:10 am
aave-api ✅ Success (View Logs) Jul 30, 2026 at 1:10 am
tornado-api ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
gitcoin-api ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
fluid-api ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
ens-api ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
lil-nouns-api ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
compound-api ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
docs ✅ Success (View Logs) Web Jul 30, 2026 at 1:09 am
scroll-api ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
obol-api ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
authful ✅ Success (View Logs) Web Jul 30, 2026 at 1:09 am
otelcol ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
mcp ✅ Success (View Logs) Web Jul 30, 2026 at 1:09 am
grafana ✅ Success (View Logs) Web Jul 30, 2026 at 1:09 am
loki ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
alertmanager ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
prometheus ✅ Success (View Logs) Jul 30, 2026 at 1:09 am
gateful ✅ Success (View Logs) Web Jul 30, 2026 at 1:08 am
tempo ✅ Success (View Logs) Jul 30, 2026 at 1:08 am
user-api ✅ Success (View Logs) Web Jul 29, 2026 at 5:44 pm
scroll-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:55 am
uniswap-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:54 am
shutter-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:54 am
ens-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:54 am
compound-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:54 am
tornado-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:54 am
aave-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:54 am
lil-nouns-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:53 am
fluid-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:53 am
gitcoin-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:53 am
obol-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:53 am
nouns-indexer ✅ Success (View Logs) Jul 28, 2026 at 11:52 am
erpc ✅ Success (View Logs) Web Jul 27, 2026 at 11:35 am
uniswap-indexer-offchain ✅ Success (View Logs) Jul 24, 2026 at 1:21 am
gitcoin-indexer-offchain ✅ Success (View Logs) Jul 24, 2026 at 1:21 am
shutter-indexer-offchain ✅ Success (View Logs) Jul 24, 2026 at 1:21 am
compound-indexer-offchain ✅ Success (View Logs) Jul 24, 2026 at 1:17 am
ens-indexer-offchain ✅ Success (View Logs) Jul 24, 2026 at 1:17 am
address-enrichment ✅ Success (View Logs) Web Jul 23, 2026 at 11:14 pm
ens-relayer ✅ Success (View Logs) Jul 23, 2026 at 11:14 pm
nodeful ✅ Success (View Logs) Jul 23, 2026 at 11:13 pm

Address enrichment (ENS, contract flag, arkham labels) had no staleTime, and
the global QueryClient defaults to 0, so every EnsAvatar/TypeCell refetched on
each remount. Table re-renders (amplified by the inactive banner and per-row
activity fetch) remounted rows and fired/canceled these requests repeatedly,
flooding the address-enrichment API on every tab open. Give the useGetAddress
and useGetAddresses calls a 5m staleTime / 30m gcTime so identical addresses
dedupe and stay cached across remounts.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
- votes: rename user-vote filter labels For/Against to Yes/No
- voting power: DS SegmentedControl for current/former view
- voting power: summary (Current VP / Total VP Lost) on the selector row
- former delegators: short date (Jan 3, 25), dedup VP impact when unchanged
- delegation history: net VP change on graph, low-importance toggle on CSV row
- activity: DS SegmentedControl for date and relevance
- token holders: banner uses DS InlineAlert, table fills height below it
- top interactions: hide-dust on CSV row, total as USD, Net Tokens In/Out (90D)
- balance history: net balance change value on graph
- period label: All time renamed to Max available data
- table: footerActions slot; inline alert accepts ReactNode content

Co-Authored-By: Claude Opus 4.8 <[email protected]>
brunod-e and others added 2 commits July 27, 2026 09:57
- EnsAvatar: optional subtitle slot rendered under the name; the avatar
  stays vertically centered against the whole name + subtitle block
- token holders: delegate column renders "Voted X/Y" via the subtitle slot
  so the avatar aligns with both lines
- table: row dividers now live on the cells, since border-separate tables
  do not paint borders set on the <tr>

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Verified against the Figma frame measured pixel by pixel, and against the
locally rendered table:

- delegate cell: avatar is vertically centered against the whole
  name + "Voted x/y" block (measured offset 0), and the subtitle starts at
  the same x as the name, as in the design
- row borders: border-separate tables never paint borders declared on the
  <tr>, and the first cell additionally cleared them on desktop, so the
  divider was missing entirely and never reached the Address column. The
  line is now drawn by a cell pseudo element spanning the full cell width,
  ignoring the horizontal padding.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
- address column: long arkham labels are truncated instead of overflowing. The
  chain was broken by the tooltip trigger button, an inline-block whose
  shrink-to-fit width was set by the label, so `truncate` never had a width to
  work with. Clamping the trigger fixes every table using EnsAvatar.
- avg vote timing: shows a skeleton while the per-row proposals activity loads,
  instead of the "-" it uses for delegates with no votes.
- calendar popover: uses rounded-base, so the radius follows the DS token
  (0 for Anticapture, non-zero for whitelabels) instead of a hardcoded md.
- token holders change column: right aligned, matching the delegates tab.
- balance history and voting power graphs: the heading no longer changes between
  loading and loaded states.
- drawer activity: the feed is scoped to the inspected wallet again. The address
  filter was fine; "All" omitted `relevance`, which the API reads as MEDIUM, so
  its value thresholds hid almost everything. The API now takes relevance=ALL to
  drop the threshold, and the drawer always sends the value explicitly.
- drawer activity: infinite scroll works. The observer used the viewport as root
  while the list scrolls in its own container, leaving the sentinel on the
  clipped edge and the feed stuck on page one.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ment

- holders and delegates page title reads "Stakeholders", matching its nav entry
- delegate votes: the rate metric card is labelled "For Rate"
- former delegators: VP Impact header aligns left
- voting power summary: the loading skeleton aligns left with the value it
  replaces

Co-Authored-By: Claude Opus 4.8 <[email protected]>
An ENS record can be a 42 character address plus ".eth", which overflowed the
address column and ran into the next one. DrawerAddressButton wraps the avatar
in a button, and a button is inline-block, so it was sized by its content and
the name below it never had a width to truncate against. Clamping it fixes every
drawer table that renders an address.

VP Impact is centered in both the header and the cells.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
brunod-e and others added 2 commits July 29, 2026 11:03
API:
- proposals-activity accepts an optional `toDate`, enforced on the proposal
  timestamp in both the page and the analytics queries. The dashboard already
  sent it for custom ranges, where it was silently discarded, so bounded
  periods counted every proposal through today.
- the feed's delegation enrichment keeps the row that mentions the filtered
  address. Partial delegations (SCR) write one row per delegatee out of a
  single DelegateChanged, all sharing tx hash and log index, so collapsing
  them by key could describe a delegate unrelated to the filtered address.

Dashboard:
- balance-change and voting-power-change totals read their period boundaries
  from their own limit-1 lookups instead of the plotted rows, which are capped
  at 1,000 and hide small events. Active accounts were reporting the change
  over a truncated suffix of the period.
- "Hide dust" moves into the interactions query. Filtering client-side could
  empty a page, and an empty table drops the infinite-scroll sentinel, leaving
  qualifying rows on later pages unreachable.
- per-address activity fetches carry a generation for the DAO and range, so a
  superseded response can no longer merge stale proposal counts into the rows.
- the drawer's Activity tab only renders for DAOs whose API serves the feed;
  AAVE showed a permanent error state. An unknown tab in the URL now falls
  back to the first one instead of an empty body.
- clicking an address in the drawer feed carries its entity type, so a
  delegate opens the delegate profile rather than the token-holder tabs.
- the custom range calendar can apply a single day, via an explicit Apply.
  react-day-picker answers the first click with `from` equal to `to`, so the
  old inequality check made one specific day unselectable.

Co-Authored-By: Claude Opus 5 <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c038ed7139

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/repositories/feed/index.ts
Comment thread apps/dashboard/features/holders-and-delegates/HoldersAndDelegatesSection.tsx Outdated
brunod-e added 2 commits July 29, 2026 13:22
…0860)

Every in-drawer address column now states which kind of profile it points
at, so DrawerAddressButton sets the drawerEntity override the same way the
drawer's activity feed does. Clicking a delegator in a delegate drawer no
longer opens that holder with delegate tabs, and clicking a delegate from a
token holder's delegation history no longer keeps token holder tabs.
The holders and delegates tab param is parsed as an enum instead of a plain
string, so a stale or hand-edited ?tab=foo coerces back to the default tab
rather than missing every key in tabComponentMap and rendering an empty
section body.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60daa3c9f1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/dashboard/features/holders-and-delegates/components/DrawerAddressButton.tsx Outdated
…eview #3675140866)

Partial delegation writes one delegations row per delegatee out of a single
DelegateChanged, so the primary row alone describes the event badly: an
unfiltered feed renders one arbitrary delegatee, and a feed filtered by the
delegator matches every sibling row, so picking one drops the others.

FeedDelegationMetadata gains an optional 'delegatees' array of
{ delegate, amount }, ordered by delegate address ascending and present only
when the event has more than one row. 'delegate', 'amount' and
'previousDelegate' keep their meaning and still come from the primary row that
indexDelegationsByKey selects, so the deployed client and the dashboard feed
renderer are unaffected.
brunod-e added 2 commits July 29, 2026 13:42
…ages (review #3676245485)

"Hide dust" is on by default and enforced by the query, so an address whose
every interaction is under $1 came back empty and the early return took the
whole table away, footer switch included, with no way to turn the filter back
off. The table now stays mounted and shows an empty state that names the filter
responsible. TopInteractions owns the genuinely-no-interactions case instead:
its query carries no filters, so it hides the table and shows its blank slate
alone rather than stacking two empty states.
…ew #3676245492)

The override was a bare entity type, so any path that cleared drawerAddress
without clearing it too (the section's tab cleanup, each parent's onClose) left
it behind for the next address opened from a table, which then rendered the
wrong profile's tabs.

It is now recorded as '<entityType>:<address>' and honored only while the
recorded address matches the drawer's, compared case-insensitively since the
two come from different sources. Re-pointing the drawer drops the override by
itself, so no cleanup path has to remember it, and a future one cannot
reintroduce the bug. Encoding lives in a single useDrawerEntityOverride hook
shared by both writers; DrawerActivityFeed writes through it directly, which
retires the onEntityTypeChange prop that could not carry the address.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/blockful/anticapture/blob/40f44142a6027c30d14818203b7b62de3cf587b3//tmp/anticapture-e7a/apps/dashboard/features/holders-and-delegates/components/DrawerActivityFeed.tsx#L105-L107
P2 Badge Re-point drawers through the owner state

When the profile drawer is opened from ActivityFeedSection, its address comes from local drawerState, not the drawerAddress query param. In that context, clicking an address inside this new drawer Activity tab only updates the URL here, so the visible drawer remains on the old address; pass a re-point callback down from the drawer owner (or make all owners read the same state) instead of writing only the query param.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23c90fe6b5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/api/src/mappers/feed/index.ts
Drop comments that restated the code they sat on: JSX section labels
(Filters, Timeline, Delegators), component headers that paraphrased the
component name, and a guard comment narrating its own condition.

Tighten the remaining ones to the reason the code is the way it is, and
drop the trailing ticket refs, which point at the PR rather than at the
code.

Comments only, no behavior change.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd8d62ad26

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +348 to +349
setMinValue(filterState.minAmount || null);
setMaxValue(filterState.maxAmount || null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter AAVE delegation received by the displayed value

When a user applies the new min/max filter on the AAVE “Delegation received” column, these values are sent as fromValue/toValue, but the AAVE voting-power repository applies that amount filter to combinedPowerSql (delegated voting power plus wallet balance) while this table displays combinedPower - balance as Delegation received. For AAVE addresses with a large self balance but little or no delegated power, the filter can include rows that visibly do not satisfy the column filter, and max filters can exclude delegated rows because their own balance pushes the combined total over the threshold; filter the delegated-power expression or move this filter to the Total column.

Useful? React with 👍 / 👎.

Comment on lines +98 to +100
const userMin = filterVariables.minAmount
? BigInt(filterVariables.minAmount)
: 0n;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard malformed minAmount before BigInt

Because minAmount comes from the URL, a stale or hand-edited value such as ?minAmount=1.5 or ?minAmount=abc now throws in render before the API/query layer can handle it, taking down the Top Interactions drawer. The other newly added amount filters treat URL values as user-editable and catch parse failures; do the same here or ignore invalid values before calling BigInt.

Useful? React with 👍 / 👎.

…iew)

The AAVE delegates table renders `combined - balance` as "Delegation
received", but the repository applied `fromValue`/`toValue` to the combined
total (delegated power plus the account's own balance). A large self balance
alone could satisfy a minimum, and it could push a genuinely delegated
account past a maximum. Filter the delegated power expression instead, which
also matches what `orderBy=votingPower` already sorts by on the same
endpoint, and what every other DAO does.

Also guard the Top Interactions `minAmount`/`maxAmount` parse: both come
from the URL, so a stale or hand-edited `?minAmount=1.5` threw inside
`BigInt()` while rendering and took the drawer down. Invalid values are now
ignored.

Co-Authored-By: Claude Opus 5 <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b6c836cb0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 181 to 184
LEFT JOIN votes_onchain v ON p.id = v.proposal_id AND v.voter_account_id = ${address}
WHERE (p.timestamp + ${votingPeriodSeconds}) >= ${activityStart}
${this.activityEndCondition(activityEnd, "p.timestamp")}
AND UPPER(p.status) <> 'CANCELED'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound activity votes to the selected range

For bounded ranges this only removes proposals opened after toDate; the joined votes_onchain rows are not constrained by v.timestamp (and the later analytics getUserVotes query has the same shape). When a custom range ends before an overlapping proposal closes, a delegate vote cast after the selected end date still populates userVote and increments votedProposals, so delegates can be shown as active despite the UI’s “no votes cast in the selected period” semantics. Add vote timestamp bounds for the selected range or change the metric/copy.

Useful? React with 👍 / 👎.

Comment on lines +35 to +39
WHERE NOT EXISTS (
SELECT 1
FROM votes_onchain v
WHERE v.voter_account_id = ap.account_id
AND v.proposal_id IN (SELECT id FROM window_proposals)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound inactive-summary votes to the selected range

This aggregate has the same bounded-range mismatch: window_proposals is date-limited, but any vote ever cast on those proposal IDs makes the delegate active. If a delegate votes after toDate on a proposal that opened inside the custom range, their current voting power is omitted from inactiveDelegatedVotingPower, so the banner understates inactive delegated VP for that range. Include v.timestamp in the from/to predicate before using the vote to satisfy EXISTS.

Useful? React with 👍 / 👎.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants