Skip to content

feat(APP-667): Pull token voting ERC20 members from aragon-subdomain - #1128

Open
asciiman wants to merge 36 commits into
mainfrom
app-667-pull-token-voting-erc20-members-from-aragon-subdomain
Open

feat(APP-667): Pull token voting ERC20 members from aragon-subdomain#1128
asciiman wants to merge 36 commits into
mainfrom
app-667-pull-token-voting-erc20-members-from-aragon-subdomain

Conversation

@asciiman

@asciiman asciiman commented May 9, 2026

Copy link
Copy Markdown
Contributor

Description

This repoints the mainnet ERC-20 Token Voting member list from the legacy backend to the aragon-domain package as the first slice of the platform architecture migration.

Requests are routed per plugin. Eligible token-voting queries go to the new domain BFF. Everything else falls back to the legacy backend unchanged. Both sources return the same library-owned DTO.

What's in this PR

  • aragon-domain new serves member lists. Mainnet plain-ERC-20 Token Voting membership is fetched from the aragon-domain through a new BFF route. Other networks, wrapped/VE tokens, lock-to-vote, multisig and admin keep using the legacy backend via a single routing predicate.
  • Package DTOs as the contract: Member lists are typed with DTO pages from the aragon-domain regardless of source. Legacy responses are normalized through an anti-corruption mapper, and member activity now renders from indexed timestamps instead of per-block RPC lookups.
  • SSR hydration for member lists: The members page prefetches the exact query key the token/lock-to-vote list consumes. There is a server options twin calling the domain controller in-process. The first paint comes from the server render instead of a client refetch.
  • Reusable domain-service plumbing: The integration follows the aragonDomainService pattern previously introduced. A client/server service pair over a lazy server singleton now wires per-network RPC URLs and exposes the full controller via getDomain() instead of per-method delegates.

Where to look

Start with governanceService/utils/fetchTokenVotingMembership. This is the single routing implementation both the client fetch and the RSC prefetch share, with the domain transport injected. resolveMemberSource holds the eligibility rules, and buildTokenVotingMembershipParams carries the server/client query-key contract. The two sides must build identical params.

Notes

  • daoUtils.isLinkedAccountPlugin now uses a server-safe address comparison. The gov-ui-kit shim's exports aren't callable from RSC context, and the members-page prefetch is its first server-side caller.
  • Lock-to-vote uses the token-voting membership query because both lists render through TokenMemberListBase, and the routing predicate always sends LTV to the legacy backend. Moving LTV to its own query is deferred until the aragon-domain adds a lock-to-vote membership use case with its own DTO, so that code path only needs to be built once.

Type of Change

  • Major: Breaking change (change that would cause existing functionality to not work as expected)
  • Minor: Feature (non-breaking change which adds new functionality)
  • Patch: Enhancement (non-breaking change to an existing feature)
  • Patch: Bug fix (non-breaking change which fixes an issue)

Developer Checklist:

  • Manually smoke tested the functionality in a preview or locally
  • Confirmed there are no new warnings or errors in the browser console
  • (For User Stories only) Double-checked that all Acceptance Criteria are satisfied
  • Confirmed there are no new warnings on automated tests
  • Merged and published any dependent changes in downstream modules
  • Selected the correct base branch
  • Commented the code in hard-to-understand areas
  • Followed the code style guidelines of this project
  • Reviewed that the Files Changed in Github’s UI reflect my intended changes
  • Confirmed the pipeline checks are not failing

Review Checklist:

  • (For User Stories only) Tested in a preview or locally that all Acceptance Criteria are satisfied
  • Confirmed that changes follow the code style guidelines of this project

@linear

linear Bot commented May 9, 2026

Copy link
Copy Markdown

APP-667

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

🚀 Preview Deployment: View Here

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

E2E results (preview)

Smoke

Base URL https://app-next-oyo7mgxzd-aragon-app.vercel.app
Suite smoke
Playwright result ❌ failed
Summary 80 passed, 2 failed
HTML report View report
GitHub job ✅ completed

View run #4873

asciiman and others added 3 commits July 29, 2026 12:51
…-aragon-subdomain

Brings the branch across the monorepo restructure (apps/app) and reconciles
the two aragon-domain integrations:

- Relocated all branch-added files from src/ into apps/app/src/ (per MIGRATION.md).
- Kept @aragon/aragon-domain pinned to the 0.0.0-20260626160332 snapshot
  (superset of 0.3.1: adds getTokenVotingMembership + required rpcUrls load arg);
  main's ^0.3.1 dropped, lockfile regenerated.
- .env.example: adopted main's NEXT_SECRET_ENVIO_* variable names.
- Dropped branch-local graphql/graphql-request/ddd-core-ts direct deps
  (declared as regular dependencies of the package itself).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ainService pattern

Aligns the branch's aragon-domain integration with the service architecture
main established in APP-795, replacing the ad-hoc route + raw fetch:

- aragonDomainService.backend singleton now passes the required rpcUrls to
  AragonDomain.load and exposes getTokenVotingMembership.
- New tokenVotingMembershipService client/server/api triplet typed entirely
  with package DTOs; BFF route moves from /api/subdomain/members to
  /api/domain/token-voting/members with monitoring on failure.
- Source routing consolidated into a single fetchTokenVotingMembership util
  (domain transport injected: BFF client on the browser, in-process
  controller via tokenVotingMembershipOptionsServer for RSC prefetch).
- getMemberList reverted to its generic pre-branch form; routing fields move
  to IGetTokenVotingMembershipQueryParams; tokenProcessBodyField now uses the
  dedicated membership hook for its member count.
- daoMembersPage prefetches the token-voting membership query for
  token-voting/lock-to-vote plugins via the shared
  buildTokenVotingMembershipParams helper, restoring SSR cache hydration for
  those lists.
- "subdomain" vocabulary renamed to "domain" (resolveMemberSource,
  DOMAIN_NETWORKS) to avoid clashing with ENS subdomains.
- Temporary aragonDomain.d.ts augmentation bridges the two member-profile DTO
  exports missing from the pinned snapshot; delete on the next published
  aragon-domain version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new snapshot exports the member-profile DTOs that were missing from the
6/26 snapshot, so the temporary aragonDomain.d.ts module augmentation is
deleted with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
asciiman and others added 3 commits July 31, 2026 16:57
…embershipParams

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gMembershipParams

Replaces the defensive `settings as { token?: ... }` read with a typed
contract: the helper takes IDaoPlugin<ITokenVotingMembershipPluginSettings>,
and the members page narrows its generic plugin through the new
isTokenVotingMembershipPlugin type guard (the same interfaceType→settings
contract the slot registry relies on). tokenMemberListBase already carries
the stronger type and is unchanged. A discriminated-union IDaoPlugin would
make the guard unnecessary — noted in the guard's jsdoc as the future fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…berListPlugin

The plugin set is defined by which component renders the list
(TokenMemberListBase serves both token-voting and lock-to-vote), not by the
TokenVoting plugin identity — the old name read as the latter. Query and
params names stay tied to the aragon-domain DTO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/app/package.json Outdated
},
"dependencies": {
"@aragon/aragon-domain": "^0.3.1",
"@aragon/aragon-domain": "0.0.0-20260730113648",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Don't merge until this is updated with release build of lib.

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.

Not approving yet, your own note still stands: the dep's on the dev snapshot 0.0.0-20260730113648, so release build (and a rebase onto main) before this goes. Code side's solid, a couple of type nits inline.

@asciiman
asciiman marked this pull request as ready for review August 6, 2026 08:17
@asciiman
asciiman requested a review from a team as a code owner August 6, 2026 08:17
pluginInterfaceType: PluginInterfaceType.TOKEN_VOTING,
tokenAddress: membership.token.address,
tokenUnderlying:
(membership.token as { underlying?: string | null })

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.

This cast is quietly doing the routing. ITokenSetupMembershipForm.token doesn't declare underlying, and the value only survives because tokenBodyUtils spreads the full settings token in. If someone later builds membership.token to the declared shape, a mainnet wrapped or VE body flips to the domain source and shows the wrong holder count with no warning. Can we declare underlying on the form token type (picked off ITokenPluginSettingsToken) so the compiler catches it?

network != null &&
DOMAIN_NETWORKS.has(network) &&
interfaceType === PluginInterfaceType.TOKEN_VOTING &&
tokenUnderlying == null;

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.

The VE exclusion leans on VE tokens always carrying underlying, but the type only promises that for the wrap-needed case, and votingEscrow never reaches the routing params. If a mainnet VE deployment ever ships with underlying null, its list reads plain ERC20Votes balances from the domain instead of escrow locks. Threading a votingEscrow flag through buildTokenVotingMembershipParams and excluding on it here keeps the predicate self-contained.

const { queryParams } = params;
const { tokenAddress, page, pageSize } = queryParams;

if (tokenAddress != null && resolveMemberSource(queryParams) === 'domain') {

@thekidnamedkd thekidnamedkd Aug 6, 2026

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.

When the domain fetch throws, mainnet lists now hard-error where legacy used to serve them, since this slice has no runtime fallback. If that's on purpose so indexer problems surface instead of hiding behind legacy data, all good.

@@ -1,2 +1,3 @@
export { DaoMembersPage, type IDaoMembersPageProps } from './daoMembersPage';
// The DaoMembersPage RSC is NOT exported here on purpose since imports

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.

nit: missing word in the comment.

Suggested change
// The DaoMembersPage RSC is NOT exported here on purpose since imports
// The DaoMembersPage RSC is NOT exported here on purpose since it imports

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