Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
28 changes: 28 additions & 0 deletions apps/api-gateway/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ type Query {
"""Returns the active delegates that did not vote on a given proposal"""
proposalNonVoters(id: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposalNonVoters_orderDirection = desc, addresses: JSON): proposalNonVoters_200_response

"""Returns a paginated list of votes cast on a specific proposal"""
proposalVotes(id: String!, skip: NonNegativeInt, limit: PositiveInt = 10, account: String!, sortBy: queryInput_proposalVotes_sortBy = timestamp, sortOrder: queryInput_proposalVotes_sortOrder = desc, support: Int): proposalVotes_200_response

"""
Fetch historical token balances for multiple addresses at a specific time period using multicall
"""
Expand Down Expand Up @@ -1742,6 +1745,31 @@ enum queryInput_proposalNonVoters_orderDirection {
desc
}

type proposalVotes_200_response {
items: [query_proposalVotes_items_items]!
totalCount: Float!
}

type query_proposalVotes_items_items {
voter: String!
transactionHash: String!
proposalId: String!
support: Float!
votingPower: String!
reason: String
timestamp: Float!
}

enum queryInput_proposalVotes_sortBy {
timestamp
votingPower
}

enum queryInput_proposalVotes_sortOrder {
asc
desc
}

type query_historicalBalances_items {
address: String!
balance: String!
Expand Down
1 change: 1 addition & 0 deletions apps/api-gateway/src/resolvers/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const daoItemQueries = [
"proposal",
"proposalNonVoters",
"proposals",
"proposalVotes",
"proposalsActivity",
"token",
"transactions",
Expand Down
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, support } =
context.req.valid("query");

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

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")
.optional()
.default(10),
account: z.string().refine((val) => isAddress(val)),
sortBy: z.enum(["timestamp", "votingPower"]).default("timestamp").optional(),
sortOrder: z.enum(["asc", "desc"]).default("desc").optional(),
support: 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,
support?: number,
): Promise<number> {
const whereClauses: SQL<unknown>[] = [
eq(votesOnchain.proposalId, proposalId),
eq(votesOnchain.voterAccountId, account),
];

if (support !== undefined) {
whereClauses.push(eq(votesOnchain.support, support.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,
support?: number,
): Promise<DBVote[]>;
getProposalVotesCount(
proposalId: string,
account?: Address,
support?: 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,
support?: number,
): Promise<VotesResponse> {
const [votes, totalCount] = await Promise.all([
this.proposalsRepo.getProposalVotes(
proposalId,
skip,
limit,
sortBy,
sortOrder,
account,
support,
),
this.proposalsRepo.getProposalVotesCount(proposalId, account, support),
]);

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
}
}