Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
47 changes: 47 additions & 0 deletions apps/indexer/src/api/controllers/proposals/proposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
ProposalMapper,
VotersRequestSchema,
VotersResponseSchema,
VotesRequestSchema,
VotesResponseSchema,
} from "@/api/mappers";
import { DAOClient } from "@/interfaces";

Expand Down Expand Up @@ -162,4 +164,49 @@ export function proposals(
return context.json({ totalCount, items });
},
);

app.openapi(
createRoute({
method: "get",
operationId: "proposalVotes",
path: "/proposals/{id}/votes",
summary: "List of votes for a given proposal",
description:
"Returns a paginated list of votes cast on a specific proposal",
tags: ["proposals"],
request: {
params: z.object({
id: z.string(),
}),
query: VotesRequestSchema,
},
responses: {
200: {
description: "Successfully retrieved votes",
content: {
"application/json": {
schema: VotesResponseSchema,
},
},
},
},
}),
async (context) => {
const { id } = context.req.valid("param");
const { skip, limit, account, sortBy, sortOrder, reason } =
context.req.valid("query");

const { totalCount, items } = await service.getProposalVotes(
id,
skip,
limit,
sortBy,
sortOrder,
account,
reason,
);

return context.json({ totalCount, items });
},
);
}
1 change: 1 addition & 0 deletions apps/indexer/src/api/mappers/proposals/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./voters";
export * from "./proposals";
export * from "./votes";
60 changes: 60 additions & 0 deletions apps/indexer/src/api/mappers/proposals/votes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { z } from "@hono/zod-openapi";
import { votesOnchain } from "ponder:schema";
import { isAddress } from "viem";

export type DBVote = typeof votesOnchain.$inferSelect;

export const VotesRequestSchema = z.object({
skip: z.coerce
.number()
.int()
.min(0, "Skip must be a non-negative integer")
.optional()
.default(0),
limit: z.coerce
.number()
.int()
.min(1, "Limit must be a positive integer")
.max(1000, "Limit cannot exceed 1000")
.default(10)
.optional(),
account: z.string().refine((val) => isAddress(val)),
sortBy: z.enum(["timestamp", "votingPower"]).default("timestamp").optional(),
sortOrder: z.enum(["asc", "desc"]).default("desc").optional(),
reason: z.coerce.number().int().optional(),
});

export type VotesRequest = z.infer<typeof VotesRequestSchema>;

export const VoteResponseSchema = z.object({
voter: z.string(),
transactionHash: z.string(),
proposalId: z.string(),
support: z.number(),
votingPower: z.string(),
reason: z.string().nullable(),
timestamp: z.number(),
});

export type VoteResponse = z.infer<typeof VoteResponseSchema>;

export const VotesResponseSchema = z.object({
items: z.array(VoteResponseSchema),
totalCount: z.number(),
});

export type VotesResponse = z.infer<typeof VotesResponseSchema>;

export const VotesMapper = {
toApi: (vote: DBVote): VoteResponse => {
return {
voter: vote.voterAccountId,
transactionHash: vote.txHash,
proposalId: vote.proposalId,
support: Number(vote.support),
votingPower: vote.votingPower.toString(),
reason: vote.reason || null,
timestamp: Number(vote.timestamp),
};
},
};
52 changes: 51 additions & 1 deletion apps/indexer/src/api/repositories/drizzle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
VotesCompareQueryResult,
} from "@/api/controllers";
import { DaysEnum } from "@/lib/enums";
import { DBProposal } from "@/api/mappers";
import { DBProposal, DBVote } from "@/api/mappers";

export class DrizzleRepository {
async getSupplyComparison(metricType: string, days: DaysEnum) {
Expand Down Expand Up @@ -308,6 +308,56 @@ export class DrizzleRepository {
);
}

async getProposalVotes(
proposalId: string,
skip: number,
limit: number,
sortBy: "timestamp" | "votingPower",
sortOrder: "asc" | "desc",
account: Address,
reason?: number,
): Promise<DBVote[]> {
const whereClauses: SQL<unknown>[] = [
eq(votesOnchain.proposalId, proposalId),
eq(votesOnchain.voterAccountId, account),
];

if (reason !== undefined) {
whereClauses.push(eq(votesOnchain.support, reason.toString()));
}

const orderByColumn =
sortBy === "votingPower"
? votesOnchain.votingPower
: votesOnchain.timestamp;
const orderFn = sortOrder === "asc" ? asc : desc;

return await db
.select()
.from(votesOnchain)
.where(and(...whereClauses))
.orderBy(orderFn(orderByColumn))
.limit(limit)
.offset(skip);
}

async getProposalVotesCount(
proposalId: string,
account: Address,
reason?: number,
): Promise<number> {
const whereClauses: SQL<unknown>[] = [
eq(votesOnchain.proposalId, proposalId),
eq(votesOnchain.voterAccountId, account),
];

if (reason !== undefined) {
whereClauses.push(eq(votesOnchain.support, reason.toString()));
}

return await db.$count(votesOnchain, and(...whereClauses));
}

now() {
return Math.floor(Date.now() / 1000);
}
Expand Down
61 changes: 60 additions & 1 deletion apps/indexer/src/api/services/proposals/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { DBProposal, ProposalsRequest, VotersResponse } from "@/api/mappers";
import {
DBProposal,
ProposalsRequest,
VotersResponse,
VotesResponse,
DBVote,
} from "@/api/mappers";
import { DAOClient } from "@/interfaces/client";
import { ProposalStatus } from "@/lib/constants";
import { DaysEnum } from "@/lib/enums";
Expand Down Expand Up @@ -29,6 +35,20 @@ interface ProposalsRepository {
voters: Address[],
comparisonTimestamp: number,
): Promise<Record<Address, bigint>>;
getProposalVotes(
proposalId: string,
skip: number,
limit: number,
sortBy: "timestamp" | "votingPower",
sortOrder: "asc" | "desc",
account?: Address,
reason?: number,
): Promise<DBVote[]>;
getProposalVotesCount(
proposalId: string,
account?: Address,
reason?: number,
): Promise<number>;
}

export class ProposalsService {
Expand Down Expand Up @@ -170,4 +190,43 @@ export class ProposalsService {
})),
};
}

/**
* Returns the list of votes for a given proposal
*/
async getProposalVotes(
proposalId: string,
skip: number = 0,
limit: number = 10,
sortBy: "timestamp" | "votingPower" = "timestamp",
sortOrder: "asc" | "desc" = "desc",
account?: Address,
reason?: number,
): Promise<VotesResponse> {
const [votes, totalCount] = await Promise.all([
this.proposalsRepo.getProposalVotes(
proposalId,
skip,
limit,
sortBy,
sortOrder,
account,
reason,
),
this.proposalsRepo.getProposalVotesCount(proposalId, account, reason),
]);

return {
totalCount,
items: votes.map((vote) => ({
voter: vote.voterAccountId,
transactionHash: vote.txHash,
proposalId: vote.proposalId,
support: Number(vote.support),
votingPower: vote.votingPower.toString(),
reason: vote.reason || null,
timestamp: Number(vote.timestamp),
})),
};
}
}
55 changes: 0 additions & 55 deletions packages/graphql-client/documents/governance/proposals.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -55,50 +55,6 @@ query GetProposal($id: String!) {
}
}

# Get proposal votes
query GetVotesOnchains(
$proposalId: String
$limit: Int
$after: String
$before: String
$orderBy: String = "timestamp"
$orderDirection: String = "desc"
) {
votesOnchains(
limit: $limit
after: $after
before: $before
where: { proposalId: $proposalId }
orderBy: $orderBy
orderDirection: $orderDirection
) {
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
totalCount
items {
voterAccountId
txHash
daoId
proposalId
support
votingPower
reason
timestamp
}
}
}

# Get proposal votes
query GetVotesOnchainsTotalCount($proposalId: String) {
votesOnchains(where: { proposalId: $proposalId }) {
totalCount
}
}

# Get voting power change
query GetVotingPowerChange(
$addresses: JSON!
Expand Down Expand Up @@ -153,14 +109,3 @@ query GetAccountPower($address: String!, $proposalId: String!) {
daoId
}
}

query GetUserVote($proposalId: String!, $address: String!) {
votesOnchain(proposalId: $proposalId, voterAccountId: $address) {
support
votingPower
reason
timestamp
txHash
daoId
}
}