Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1209f32
Pull token voting erc20 members from aragon-subdomain
asciiman May 8, 2026
4151be8
Now route all mainnet tokenvoting erc20 member from subdomain
asciiman May 8, 2026
9b6e471
Only route erc20 token voting plugins to subdomain
asciiman May 9, 2026
264202c
Remove ENS_SUBGRAPH_URL dependency from member route
asciiman May 11, 2026
7cfcaa8
Use published aragon-subdomain
asciiman May 19, 2026
2a4e536
Merge branch 'main' into app-667-pull-token-voting-erc20-members-from…
asciiman Jun 16, 2026
0929ffe
Merge branch 'main' into app-667-pull-token-voting-erc20-members-from…
asciiman Jun 16, 2026
689ff7f
Update the subdomain lib
asciiman Jun 19, 2026
5d98bbc
Use aragon-domain types for token voting
asciiman Jun 23, 2026
5ae37b6
Change package from aragon-subdomain to aragon-domain
asciiman Jun 23, 2026
a75e70f
Fix import
asciiman Jun 23, 2026
4d44edf
Fix mapper
asciiman Jun 23, 2026
9c8d7be
Add api key
asciiman Jun 23, 2026
a992adf
Update domain
asciiman Jun 26, 2026
802e3b3
Merge branch 'main' into app-667-pull-token-voting-erc20-members-from…
asciiman Jun 29, 2026
8055750
Merge branch 'main' into app-667-pull-token-voting-erc20-members-from…
asciiman Jul 29, 2026
ffb23b4
refactor(APP-667): re-home token-voting membership onto the aragonDom…
asciiman Jul 29, 2026
d5a58e9
chore(APP-667): bump aragon-domain to snapshot 0.0.0-20260730113648
asciiman Jul 30, 2026
5ec39fd
docs(APP-667): clarify the defensive token reads in buildTokenVotingM…
asciiman Jul 31, 2026
956e937
refactor(APP-667): require token-carrying settings in buildTokenVotin…
asciiman Jul 31, 2026
47cb89b
refactor(APP-667): rename isTokenVotingMembershipPlugin to isTokenMem…
asciiman Jul 31, 2026
31d4c18
Clean up comments
asciiman Jul 31, 2026
887f568
Fix server code being pulled into client
asciiman Aug 3, 2026
8531f72
Make linked account check server safe
asciiman Aug 4, 2026
cd6116f
Return domain from service
asciiman Aug 4, 2026
8bd171f
Resolve activity for member details
asciiman Aug 4, 2026
7c46cf4
Simplified code
asciiman Aug 4, 2026
b08e192
Docs
asciiman Aug 5, 2026
c301684
Rename hook
asciiman Aug 5, 2026
4ae75d8
Validate query params
asciiman Aug 5, 2026
5990ac9
Convert activity correctly
asciiman Aug 5, 2026
847bb3c
Update map
asciiman Aug 5, 2026
4ad218e
Docs
asciiman Aug 5, 2026
bd64317
Clean up comment
asciiman Aug 6, 2026
700c456
Merge branch 'main' into app-667-pull-token-voting-erc20-members-from…
asciiman Aug 6, 2026
d7da02e
Change domain timestamp handling
asciiman Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aragon/app": minor
---

Serve mainnet ERC-20 token-voting member lists from the aragon-domain (Envio) BFF, with source routing and a legacy-backend fallback
1 change: 1 addition & 0 deletions apps/app/config/.env.local
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# URL of the Aragon backend
ARAGON_BACKEND_URL=https://dev.backend.aragonservices.in
# ARAGON_BACKEND_URL=http://localhost:3000

# Application environment
NEXT_PUBLIC_ENV=local
Expand Down
2 changes: 1 addition & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"e2e:codegen": "playwright codegen --config e2e/playwright.config.ts"
},
"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.

"@aragon/assistant-chat": "workspace:*",
"@aragon/assistant-contracts": "workspace:*",
"@aragon/gov-ui-kit": "catalog:",
Expand Down
47 changes: 47 additions & 0 deletions apps/app/src/app/api/domain/token-voting/members/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type NextRequest, NextResponse } from 'next/server';
import { tokenVotingMembershipServiceServer } from '@/modules/governance/api/tokenVotingMembershipService/tokenVotingMembershipService.server';
import { monitoringUtils } from '@/shared/utils/monitoringUtils';

export const GET = async (req: NextRequest) => {
const params = req.nextUrl.searchParams;
const pluginAddress = params.get('pluginAddress');
const tokenContractAddress = params.get('tokenContractAddress');
const page = params.get('page');
const pageSize = params.get('pageSize');

if (pluginAddress == null || tokenContractAddress == null) {
return NextResponse.json(
{
error: 'pluginAddress and tokenContractAddress query parameters are required',
},
{ status: 400 },
);
}

try {
const result =
await tokenVotingMembershipServiceServer.getTokenVotingMembership({
queryParams: {
pluginAddress,
tokenContractAddress,
page: page != null ? Number(page) : undefined,
pageSize: pageSize != null ? Number(pageSize) : undefined,
},
});

return NextResponse.json(result);
} catch (error) {
monitoringUtils.logError(error, {
context: {
errorType: 'get_token_voting_membership_error',
pluginAddress,
tokenContractAddress,
},
});

return NextResponse.json(
{ error: 'getTokenVotingMembership request failed' },
{ status: 500 },
);
}
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DaoMembersPage } from '@/modules/governance/pages/daoMembersPage';
// Imported from the page file (not the module barrel): the RSC pulls in
// server-only prefetch code that must stay out of the barrel's client graph.
import { DaoMembersPage } from '@/modules/governance/pages/daoMembersPage/daoMembersPage';

export default DaoMembersPage;
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export interface IMemberMetrics {
/**
* Block number of the first activity of the member in the given body plugin.
* Unix-seconds timestamp of the member's first observed activity.
*/
firstActivity: number | null;
firstActivityTimestamp: number | null;
/**
* Block number of the latest activity of the member in the given body plugin.
* Unix-seconds timestamp of the member's most recent observed activity.
*/
lastActivity: number | null;
lastActivityTimestamp: number | null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {
IOrderedRequest,
IPaginatedRequest,
} from '@/shared/api/aragonBackendService';
import type { Network } from '@/shared/api/daoService';
import type { Network, PluginInterfaceType } from '@/shared/api/daoService';
import type {
IRequestQueryParams,
IRequestUrlParams,
Expand Down Expand Up @@ -59,6 +59,32 @@ export interface IGetMemberListQueryParams extends IPaginatedRequest {
export interface IGetMemberListParams
extends IRequestQueryParams<IGetMemberListQueryParams> {}

export interface IGetTokenVotingMembershipQueryParams
extends IGetMemberListQueryParams {
/**
* Network of the plugin, used to route the query to the aragon-domain BFF
* when the network is indexed by Envio.
*/
network?: Network;
/**
* Interface type of the plugin.
*/
pluginInterfaceType?: PluginInterfaceType;
/**
* Address of the governance token.
*/
tokenAddress?: string;
/**
* Address of the underlying token when the governance token is a wrapped
* or voting-escrow adapter. `null` / `undefined` means the governance
* token is a plain ERC-20.
*/
tokenUnderlying?: string | null;
}

export interface IGetTokenVotingMembershipParams
extends IRequestQueryParams<IGetTokenVotingMembershipQueryParams> {}

export interface IGetMemberUrlParams {
/**
* Address of a DAO member.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,32 @@ import { generateSppProposal } from '@/plugins/sppPlugin/testUtils/generators/sp
import { generateSppPluginSettings } from '@/plugins/sppPlugin/testUtils/generators/sppSettings';
import { generateSppStage } from '@/plugins/sppPlugin/testUtils/generators/sppStage';
import type { ISppProposal } from '@/plugins/sppPlugin/types';
import { generateTokenMember } from '@/plugins/tokenPlugin/testUtils';
import { Network, PluginInterfaceType } from '@/shared/api/daoService';
import { generatePaginatedResponse } from '@/shared/testUtils';
import {
generateMember,
generateProposal,
generateVote,
} from '../../testUtils';
import { tokenVotingMembershipServiceClient } from '../tokenVotingMembershipService';
import { governanceService } from './governanceService';
import * as fetchTokensTotalSupplyHelpers from './utils/fetchTokensTotalSupply';

describe('governance service', () => {
const requestSpy = jest.spyOn(governanceService, 'request');
const domainMembersSpy = jest.spyOn(
tokenVotingMembershipServiceClient,
'getTokenVotingMembership',
);
const fetchTokensTotalSupplySpy = jest.spyOn(
fetchTokensTotalSupplyHelpers,
'fetchTokensTotalSupply',
);

afterEach(() => {
requestSpy.mockReset();
domainMembersSpy.mockReset();
fetchTokensTotalSupplySpy.mockReset();
});

Expand All @@ -53,6 +60,144 @@ describe('governance service', () => {
expect(result).toEqual(members);
});

it('getTokenVotingMembership delegates mainnet token-voting to the aragon-domain service', async () => {
const responseBody = {
data: [
{
address: '0xabc',
ens: 'alice.eth',
votingPower: '5000',
metrics: {
firstActivityTimestamp: 1_705_320_000,
lastActivityTimestamp: 1_718_872_200,
delegationCount: 3,
},
},
],
metadata: {
page: 1,
pageSize: 10,
totalPages: 1,
totalRecords: 1,
},
};
domainMembersSpy.mockResolvedValue(responseBody);

const result = await governanceService.getTokenVotingMembership({
queryParams: {
daoId: 'dao-id-test',
pluginAddress: '0xPlugin',
tokenAddress: '0xToken',
network: Network.ETHEREUM_MAINNET,
pluginInterfaceType: PluginInterfaceType.TOKEN_VOTING,
page: 2,
pageSize: 25,
},
});

expect(requestSpy).not.toHaveBeenCalled();
expect(domainMembersSpy).toHaveBeenCalledWith({
queryParams: {
pluginAddress: '0xplugin',
tokenContractAddress: '0xtoken',
page: 2,
pageSize: 25,
},
});
expect(result).toEqual(responseBody);
});

it.each([
['non-mainnet network', { network: Network.POLYGON_MAINNET }],
[
'non-token-voting interface type',
{ pluginInterfaceType: PluginInterfaceType.MULTISIG },
],
['missing tokenAddress', { tokenAddress: undefined }],
[
'wrapped / VE-adapter governance token',
{ tokenUnderlying: '0xunderlying' },
],
])('getTokenVotingMembership routes to the legacy backend for %s', async (_label, routingOverrides) => {
requestSpy.mockResolvedValue(generatePaginatedResponse({}));
await governanceService.getTokenVotingMembership({
queryParams: {
daoId: 'dao-id-test',
pluginAddress: '0x123',
tokenAddress: '0xtoken',
network: Network.ETHEREUM_MAINNET,
pluginInterfaceType: PluginInterfaceType.TOKEN_VOTING,
...routingOverrides,
},
});

expect(domainMembersSpy).not.toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith(
governanceService['urls'].members,
expect.objectContaining({
queryParams: expect.objectContaining({
pluginAddress: '0x123',
}),
}),
);
});

it('getTokenVotingMembership maps the backend members to DTOs when routing to the legacy backend', async () => {
const member = generateTokenMember({
address: '0xabc',
ens: 'alice.eth',
votingPower: '5000',
type: 'token-voting',
firstActive: 100,
lastActive: 200,
metrics: {
firstActivityTimestamp: 1_705_320_000,
lastActivityTimestamp: 1_718_872_200,
delegationCount: 3,
},
});
requestSpy.mockResolvedValue(
generatePaginatedResponse({ data: [member] }),
);

const result = await governanceService.getTokenVotingMembership({
queryParams: {
daoId: 'dao-id-test',
pluginAddress: '0xPlugin',
tokenAddress: '0xToken',
// Non-mainnet → backend branch.
network: Network.POLYGON_MAINNET,
pluginInterfaceType: PluginInterfaceType.TOKEN_VOTING,
},
});

expect(domainMembersSpy).not.toHaveBeenCalled();
// Routing-only fields are stripped before hitting the backend.
expect(requestSpy).toHaveBeenCalledWith(
governanceService['urls'].members,
{
queryParams: {
daoId: 'dao-id-test',
pluginAddress: '0xPlugin',
},
},
);
expect(result.data).toEqual([
{
address: '0xabc',
ens: 'alice.eth',
votingPower: '5000',
metrics: {
firstActivityTimestamp: 1_705_320_000,
lastActivityTimestamp: 1_718_872_200,
delegationCount: 3,
},
},
]);
expect(result.data[0]).not.toHaveProperty('type');
expect(result.data[0]).not.toHaveProperty('firstActive');
});

it('getMember fetches the member of the specified DAO by address', async () => {
const member = generateMember({ address: '0x123' });
const params = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { PageDTO, TokenVotingMemberDTO } from '@aragon/aragon-domain';
import { invariant } from '@aragon/gov-ui-kit';
import { lockToVoteProposalUtils } from '@/plugins/lockToVotePlugin/utils/lockToVoteProposalUtils';
import { sppProposalUtils } from '@/plugins/sppPlugin/utils/sppProposalUtils';
import type { ITokenMember } from '@/plugins/tokenPlugin/types';
import {
AragonBackendService,
type IPaginatedResponse,
Expand All @@ -9,6 +11,7 @@ import type {
ICanCreateProposalResult,
IMemberExistsResult,
} from '../../types';
import { tokenVotingMembershipServiceClient } from '../tokenVotingMembershipService';
import type {
IMember,
IProposal,
Expand All @@ -24,10 +27,12 @@ import type {
IGetProposalActionsParams,
IGetProposalBySlugParams,
IGetProposalListParams,
IGetTokenVotingMembershipParams,
IGetVoteListParams,
} from './governanceService.api';
import { collectTokenAddresses } from './utils/collectTokenAddresses';
import { fetchTokensTotalSupply } from './utils/fetchTokensTotalSupply';
import { fetchTokenVotingMembership } from './utils/fetchTokenVotingMembership';

class GovernanceService extends AragonBackendService {
private urls = {
Expand All @@ -52,6 +57,21 @@ class GovernanceService extends AragonBackendService {
return result;
};

/**
* Token-voting member list. Routes between the aragon-domain BFF and the
* legacy backend (see `fetchTokenVotingMembership`), returning the
* library-owned `TokenVotingMemberDTO` page regardless of source. The
* generic `getMemberList` still serves multisig/admin.
*/
getTokenVotingMembership = (
params: IGetTokenVotingMembershipParams,
): Promise<PageDTO<TokenVotingMemberDTO>> =>
fetchTokenVotingMembership(
params,
tokenVotingMembershipServiceClient.getTokenVotingMembership,
(legacyParams) => this.getMemberList<ITokenMember>(legacyParams),
);

getMember = async <TMember extends IMember = IMember>(
params: IGetMemberParams,
): Promise<TMember> => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
IGetProposalActionsParams,
IGetProposalBySlugParams,
IGetProposalListParams,
IGetTokenVotingMembershipParams,
IGetVoteListParams,
} from './governanceService.api';

Expand All @@ -15,6 +16,7 @@ export enum GovernanceServiceKey {
PROPOSAL_ACTIONS = 'PROPOSAL_ACTIONS',
CAN_CREATE_PROPOSAL = 'CAN_CREATE_PROPOSAL',
MEMBER_LIST = 'MEMBER_LIST',
TOKEN_VOTING_MEMBERSHIP = 'TOKEN_VOTING_MEMBERSHIP',
MEMBER = 'MEMBER',
MEMBER_EXISTS = 'MEMBER_EXISTS',
VOTE_LIST = 'VOTE_LIST',
Expand All @@ -41,6 +43,10 @@ export const governanceServiceKeys = {
GovernanceServiceKey.MEMBER_LIST,
params,
],
tokenVotingMembership: (params: IGetTokenVotingMembershipParams) => [
GovernanceServiceKey.TOKEN_VOTING_MEMBERSHIP,
params,
],
member: (params: IGetMemberParams) => [GovernanceServiceKey.MEMBER, params],
memberExists: (params: IGetMemberExistsParams) => [
GovernanceServiceKey.MEMBER_EXISTS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ export {
governanceServiceKeys,
} from './governanceServiceKeys';
export * from './queries';
export {
buildTokenVotingMembershipParams,
type ITokenVotingMembershipPluginSettings,
isTokenMemberListPlugin,
} from './utils/buildTokenVotingMembershipParams';
Loading