diff --git a/.gitignore b/.gitignore index 55c782a4..2cd31512 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ coverage # Remove some common IDE working directories .idea -.vscode +.vscode \ No newline at end of file diff --git a/src/eip4824.ts b/src/eip4824.ts index b8133997..b01883c1 100644 --- a/src/eip4824.ts +++ b/src/eip4824.ts @@ -1,9 +1,9 @@ import express from 'express'; import db, { sequencerDB } from './helpers/mysql'; -import { getSpace } from './helpers/spaces'; +import { getCombinedMembersAndVoters, getSpace } from './helpers/spaces'; const router = express.Router(); -const context = ''; +const context = ['https://snapshot.org', 'https://daostar.org/schemas']; const network = process.env.NETWORK || 'testnet'; const domain = `${network === 'testnet' ? 'testnet.' : ''}snapshot.box`; const networkPrefix = network === 'testnet' ? 's-tn' : 's'; @@ -37,29 +37,93 @@ router.get('/:space', async (req, res) => { }); router.get('/:space/members', async (req, res) => { - let space: any = {}; - try { - space = await getSpace(req.params.space); + const spaceId = req.params.space; + const cursor = req.query.cursor || null; + const pageSize = 500; // Default page size + + const space = await getSpace(spaceId); + if (!space.verified) { + return res.status(400).json({ + error: 'INVALID_SPACE', + message: 'The specified space is not verified.' + }); + } - if (!space.verified) return res.status(400).json({ error: 'INVALID' }); - } catch (e) { - return res.status(404).json({ error: 'NOT_FOUND' }); - } + let members: { type: string; id: string }[] = []; + let nextCursor: string | null = null; + + if (cursor) { + const combinedMembersResult = await getCombinedMembersAndVoters( + spaceId, + cursor, + pageSize, + [], + [], + [] + ); + members = combinedMembersResult.members.map(voter => ({ + type: 'EthereumAddress', + id: voter + })); + nextCursor = combinedMembersResult.nextCursor; + } else { + const combinedMembersResult = await getCombinedMembersAndVoters( + spaceId, + cursor, + pageSize, + space.admins, + space.moderators, + space.members + ); + members = [ + ...space.admins.map(admin => ({ type: 'EthereumAddress', id: admin })), + ...space.moderators.map(moderator => ({ + type: 'EthereumAddress', + id: moderator + })), + ...space.members.map(member => ({ + type: 'EthereumAddress', + id: member + })), + ...combinedMembersResult.members.map(voter => ({ + type: 'EthereumAddress', + id: voter + })) + ]; + nextCursor = combinedMembersResult.nextCursor; + } - const members = [...space.admins, ...space.moderators, ...space.members].map( - member => ({ - type: 'EthereumAddress', - id: member - }) - ); + const responseObject = { + '@context': context, + type: 'DAO', + name: space.name, + members: members, + nextCursor: nextCursor + }; - return res.json({ - '@context': context, - type: 'DAO', - name: space.name, - members - }); + return res.json(responseObject); + } catch (e) { + const error = e as Error; + console.error(error); + + if (error.message.includes('database')) { + return res.status(500).json({ + error: 'DATABASE_ERROR', + message: 'Failed to retrieve data from the database.' + }); + } else if (error.message.includes('parameter')) { + return res.status(400).json({ + error: 'INVALID_PARAMETER', + message: 'Invalid or missing parameter.' + }); + } else { + return res.status(500).json({ + error: 'INTERNAL_SERVER_ERROR', + message: 'An unexpected error occurred.' + }); + } + } }); router.get('/:space/proposals', async (req, res) => { diff --git a/src/helpers/spaces.ts b/src/helpers/spaces.ts index 13614401..da5f3df3 100644 --- a/src/helpers/spaces.ts +++ b/src/helpers/spaces.ts @@ -241,6 +241,47 @@ async function getVotes(): Promise> { ); } +export async function getCombinedMembersAndVoters( + spaceId: string, + cursor: string | null, + pageSize: number, + knownAdmins: string[] = [], + knownModerators: string[] = [], + knownMembers: string[] = [] +) { + const params: (string | number)[] = [spaceId]; + const exclusionList = [...knownAdmins, ...knownModerators, ...knownMembers]; + + let query = 'SELECT DISTINCT voter AS address FROM votes WHERE space = ?'; + + // Add exclusion clause only if there are items to exclude + if (exclusionList.length > 0) { + query += ` AND voter NOT IN (${exclusionList.map(() => '?').join(',')})`; + params.push(...exclusionList); + } + + if (cursor) { + query += ' AND voter > ?'; + params.push(cursor); + } + + query += ' ORDER BY voter LIMIT ?'; + params.push(pageSize); + + const results = await db.queryAsync(query, params); + if (!results || results.length === 0) { + return Promise.reject(new Error('NOT_FOUND')); + } + + const nextCursor = + results.length === pageSize ? results[results.length - 1].address : null; + + return { + members: results.map(row => row.address), + nextCursor: nextCursor + }; +} + async function getFollowers(): Promise< Record > {