Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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-20260806160246",
"@aragon/assistant-chat": "workspace:*",
"@aragon/assistant-contracts": "workspace:*",
"@aragon/gov-ui-kit": "catalog:",
Expand Down
83 changes: 83 additions & 0 deletions apps/app/src/app/api/domain/token-voting/members/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/** @jest-environment node */

import type { NextRequest } from 'next/server';
import { tokenVotingMembershipServiceServer } from '@/modules/governance/api/tokenVotingMembershipService/tokenVotingMembershipService.server';
import { GET } from './route';

const pluginAddress = '0x1111111111111111111111111111111111111111';
const tokenContractAddress = '0x2222222222222222222222222222222222222222';

const generateRequest = (query: string): NextRequest =>
({
nextUrl: new URL(
`http://localhost/api/domain/token-voting/members?${query}`,
),
}) as NextRequest;

describe('GET /api/domain/token-voting/members', () => {
const getMembershipSpy = jest.spyOn(
tokenVotingMembershipServiceServer,
'getTokenVotingMembership',
);

afterEach(() => {
getMembershipSpy.mockReset();
});

it.each([
[
'a missing plugin address',
`tokenContractAddress=${tokenContractAddress}`,
],
[
'an invalid plugin address',
`pluginAddress=not-an-address&tokenContractAddress=${tokenContractAddress}`,
],
[
'an invalid token contract address',
`pluginAddress=${pluginAddress}&tokenContractAddress=0x1234`,
],
[
'a non-integer page',
`pluginAddress=${pluginAddress}&tokenContractAddress=${tokenContractAddress}&page=first`,
],
[
'a non-positive page size',
`pluginAddress=${pluginAddress}&tokenContractAddress=${tokenContractAddress}&pageSize=0`,
],
[
'a page size above the domain maximum',
`pluginAddress=${pluginAddress}&tokenContractAddress=${tokenContractAddress}&pageSize=251`,
],
])('returns 400 for %s', async (_label, query) => {
const response = await GET(generateRequest(query));

expect(response.status).toBe(400);
expect(getMembershipSpy).not.toHaveBeenCalled();
});

it('passes validated request parameters to the domain service', async () => {
const result = {
data: [],
metadata: { page: 2, pageSize: 25, totalPages: 0, totalRecords: 0 },
};
getMembershipSpy.mockResolvedValue(result);

const response = await GET(
generateRequest(
`pluginAddress=${pluginAddress}&tokenContractAddress=${tokenContractAddress}&page=2&pageSize=25`,
),
);

expect(response.status).toBe(200);
expect(getMembershipSpy).toHaveBeenCalledWith({
queryParams: {
pluginAddress,
tokenContractAddress,
page: 2,
pageSize: 25,
},
});
await expect(response.json()).resolves.toEqual(result);
});
});
82 changes: 82 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,82 @@
import { type NextRequest, NextResponse } from 'next/server';
// biome-ignore lint/style/noRestrictedImports: server-only BFF validation; strict EIP-55 validation is explicitly disabled.
import { isAddress } from 'viem';
import { tokenVotingMembershipServiceServer } from '@/modules/governance/api/tokenVotingMembershipService/tokenVotingMembershipService.server';
import { monitoringUtils } from '@/shared/utils/monitoringUtils';

const maximumPageSize = 250;

interface ITokenVotingMembershipQueryParams {
pluginAddress: string;
tokenContractAddress: string;
page?: number;
pageSize?: number;
}

const validateQueryParams = (
params: URLSearchParams,
): ITokenVotingMembershipQueryParams | undefined => {
const pluginAddress = params.get('pluginAddress');
const tokenContractAddress = params.get('tokenContractAddress');
const page = params.get('page');
const pageSize = params.get('pageSize');
const parsedPage = page != null ? Number(page) : undefined;
const parsedPageSize = pageSize != null ? Number(pageSize) : undefined;

if (
pluginAddress == null ||
tokenContractAddress == null ||
!isAddress(pluginAddress, { strict: false }) ||
!isAddress(tokenContractAddress, { strict: false }) ||
(parsedPage != null &&
(!Number.isInteger(parsedPage) || parsedPage <= 0)) ||
(parsedPageSize != null &&
(!Number.isInteger(parsedPageSize) ||
parsedPageSize <= 0 ||
parsedPageSize > maximumPageSize))
) {
return;
}

return {
pluginAddress,
tokenContractAddress,
page: parsedPage,
pageSize: parsedPageSize,
};
};

export const GET = async (req: NextRequest) => {
const queryParams = validateQueryParams(req.nextUrl.searchParams);

if (queryParams == null) {
return NextResponse.json(
{
error: 'pluginAddress and tokenContractAddress must be valid addresses; page must be a positive integer; pageSize must be a positive integer no greater than 250',
},
{ status: 400 },
);
}

try {
const result =
await tokenVotingMembershipServiceServer.getTokenVotingMembership({
queryParams,
});

return NextResponse.json(result);
} catch (error) {
monitoringUtils.logError(error, {
context: {
errorType: 'get_token_voting_membership_error',
pluginAddress: queryParams.pluginAddress,
tokenContractAddress: queryParams.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
Expand Up @@ -4,10 +4,9 @@ import type { IGetEnsTextRecordsParams } from './memberProfileService.api';

class MemberProfileServiceServer {
getEnsTextRecords = async ({ urlParams }: IGetEnsTextRecordsParams) => {
const result =
await aragonDomainServiceBackend.getMemberProfileTextRecords(
urlParams,
);
const result = await aragonDomainServiceBackend
.getDomain()
.getMemberProfileTextRecords(urlParams);

if (!result.success) {
throw new Error(
Expand Down
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
Loading
Loading