From a95b0d99bd2a0419c76a20bf070e5cf15a608d5a Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Tue, 16 Dec 2025 21:09:45 -0300 Subject: [PATCH 1/8] chore(power-gql-to-rest): add variation by accountId endpoint --- .../voting-power/voting-power-variations.ts | 51 +++++++++- .../src/api/mappers/account-balance/index.ts | 25 +---- apps/indexer/src/api/mappers/shared.ts | 19 ++++ .../voting-power/voting-power-variations.ts | 93 ++++++++++++------- .../api/repositories/voting-power/general.ts | 48 +++++++++- .../api/services/voting-power/voting-power.ts | 19 +++- 6 files changed, 193 insertions(+), 62 deletions(-) create mode 100644 apps/indexer/src/api/mappers/shared.ts diff --git a/apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts b/apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts index fc0e4e76a..63caea71d 100644 --- a/apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts +++ b/apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts @@ -1,17 +1,20 @@ import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; import { VotingPowerService } from "@/api/services"; import { - VotingPowerVariationsMapper, + VotingPowerVariationsByAccountIdRequestSchema, + VotingPowerVariationsByAccountIdResponseSchema, + VotingPowerVariationsByAccountIdMapper, VotingPowerVariationsRequestSchema, VotingPowerVariationsResponseSchema, } from "@/api/mappers/"; +import { Address } from "viem"; export function votingPowerVariations(app: Hono, service: VotingPowerService) { app.openapi( createRoute({ method: "get", operationId: "votingPowerVariations", - path: "/voting-power/variations", + path: "/voting-powers/variations", summary: "Get top changes in voting power for a given period", description: "Returns a mapping of the biggest changes to voting power associated by delegate address", @@ -41,7 +44,49 @@ export function votingPowerVariations(app: Hono, service: VotingPowerService) { orderDirection, ); - return context.json(VotingPowerVariationsMapper(result, now, days)); + return context.json( + VotingPowerVariationsByAccountIdMapper(result, now, days), + ); + }, + ); + + app.openapi( + createRoute({ + method: "get", + operationId: "votingPowerVariationsByAccountId", + path: "/voting-powers/:accountId/variations", + summary: + "Get top changes in voting power for a given period for a single account", + description: + "Returns a the changes to voting power by period and accountId", + tags: ["proposals"], + request: { + query: VotingPowerVariationsByAccountIdRequestSchema, + }, + responses: { + 200: { + description: "Successfully retrieved voting power changes", + content: { + "application/json": { + schema: VotingPowerVariationsByAccountIdResponseSchema, + }, + }, + }, + }, + }), + async (context) => { + const accountId = context.req.param("accountId"); + const { days } = context.req.valid("query"); + const now = Math.floor(Date.now() / 1000); + + const result = await service.getVotingPowerVariationsByAccountId( + accountId as Address, + now - days, + ); + + return context.json( + VotingPowerVariationsByAccountIdMapper(result, now, days), + ); }, ); } diff --git a/apps/indexer/src/api/mappers/account-balance/index.ts b/apps/indexer/src/api/mappers/account-balance/index.ts index 1cb90b816..ec32f4221 100644 --- a/apps/indexer/src/api/mappers/account-balance/index.ts +++ b/apps/indexer/src/api/mappers/account-balance/index.ts @@ -4,6 +4,7 @@ import { Address } from "viem"; import { PERCENTAGE_NO_BASELINE } from "@/api/mappers/constants"; import { CONTRACT_ADDRESSES } from "@/lib/constants"; import { calculateHistoricalBlockNumber } from "@/lib/blockTime"; +import { PeriodResponseMapper, PeriodResponseSchema } from "../shared"; export const AccountBalanceVariationsRequestSchema = z.object({ days: z @@ -28,11 +29,7 @@ export const AccountBalanceVariationsRequestSchema = z.object({ }); export const AccountBalanceVariationsResponseSchema = z.object({ - period: z.object({ - days: z.string(), - startTimestamp: z.string(), - endTimestamp: z.string(), - }), + period: PeriodResponseSchema, items: z.array( z.object({ accountId: z.string(), @@ -58,11 +55,7 @@ export const AccountInteractionsRequestSchema = }); export const AccountInteractionsResponseSchema = z.object({ - period: z.object({ - days: z.string(), - startTimestamp: z.string(), - endTimestamp: z.string(), - }), + period: PeriodResponseSchema, totalCount: z.number(), items: z.array( z.object({ @@ -140,11 +133,7 @@ export const AccountBalanceVariationsMapper = ( days: DaysEnum, ): AccountBalanceVariationsResponse => { return AccountBalanceVariationsResponseSchema.parse({ - period: { - days: DaysEnum[days] as string, - startTimestamp: new Date((endTimestamp - days) * 1000).toISOString(), - endTimestamp: new Date(endTimestamp * 1000).toISOString(), - }, + period: PeriodResponseMapper(endTimestamp, days), items: variations.map( ({ accountId, @@ -172,11 +161,7 @@ export const AccountInteractionsMapper = ( days: DaysEnum, ): AccountInteractionsResponse => { return AccountInteractionsResponseSchema.parse({ - period: { - days: DaysEnum[days] as string, - startTimestamp: new Date((endTimestamp - days) * 1000).toISOString(), - endTimestamp: new Date(endTimestamp * 1000).toISOString(), - }, + period: PeriodResponseMapper(endTimestamp, days), totalCount: interactions.interactionCount, items: interactions.interactions .filter(({ accountId: addr }) => addr !== accountId) diff --git a/apps/indexer/src/api/mappers/shared.ts b/apps/indexer/src/api/mappers/shared.ts new file mode 100644 index 000000000..1164cbc88 --- /dev/null +++ b/apps/indexer/src/api/mappers/shared.ts @@ -0,0 +1,19 @@ +import { DaysEnum } from "@/lib/enums"; +import { z } from "@hono/zod-openapi"; + +export const PeriodResponseSchema = z.object({ + days: z.string(), + startTimestamp: z.string(), + endTimestamp: z.string(), +}); + +export type PeriodResponse = z.infer; + +export const PeriodResponseMapper = ( + endTimestamp: number, + days: DaysEnum, +): PeriodResponse => ({ + days: DaysEnum[days] as string, + startTimestamp: new Date((endTimestamp - days) * 1000).toISOString(), + endTimestamp: new Date(endTimestamp * 1000).toISOString(), +}); diff --git a/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts b/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts index 130c647cd..07322f3b9 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts @@ -1,6 +1,7 @@ import { DaysEnum, DaysOpts } from "@/lib/enums"; import { z } from "@hono/zod-openapi"; import { PERCENTAGE_NO_BASELINE } from "../constants"; +import { PeriodResponseMapper, PeriodResponseSchema } from "../shared"; export const VotingPowerVariationsRequestSchema = z.object({ days: z @@ -24,27 +25,44 @@ export const VotingPowerVariationsRequestSchema = z.object({ orderDirection: z.enum(["asc", "desc"]).optional().default("desc"), }); +export const VotingPowerVariationsByAccountIdRequestSchema = z.object({ + days: z + .enum(DaysOpts) + .optional() + .default("90d") + .transform((val) => DaysEnum[val]), +}); + +export const VotingPowerVariationResponseSchema = z.object({ + accountId: z.string(), + previousVotingPower: z.string().nullish(), + currentVotingPower: z.string(), + absoluteChange: z.string(), + percentageChange: z.string(), +}); + +export const VotingPowerVariationsByAccountIdResponseSchema = z.object({ + period: PeriodResponseSchema, + data: VotingPowerVariationResponseSchema, +}); + export const VotingPowerVariationsResponseSchema = z.object({ - period: z.object({ - days: z.string(), - startTimestamp: z.string(), - endTimestamp: z.string(), - }), - items: z.array( - z.object({ - accountId: z.string(), - previousVotingPower: z.string().nullish(), - currentVotingPower: z.string(), - absoluteChange: z.string(), - percentageChange: z.string(), - }), - ), + period: PeriodResponseSchema, + items: z.array(VotingPowerVariationResponseSchema), }); +export type VotingPowerVariationResponse = z.infer< + typeof VotingPowerVariationResponseSchema +>; + export type VotingPowerVariationsResponse = z.infer< typeof VotingPowerVariationsResponseSchema >; +export type VotingPowerVariationsByAccountIdResponse = z.infer< + typeof VotingPowerVariationsByAccountIdResponseSchema +>; + export type DBVotingPowerVariation = { accountId: `0x${string}`; previousVotingPower: bigint | null; @@ -53,33 +71,36 @@ export type DBVotingPowerVariation = { percentageChange: number; }; +export const VotingPowerVariationResponseMapper = ( + delta: DBVotingPowerVariation, +): VotingPowerVariationResponse => ({ + accountId: delta.accountId, + previousVotingPower: delta.previousVotingPower?.toString(), + currentVotingPower: delta.currentVotingPower.toString(), + absoluteChange: delta.absoluteChange.toString(), + percentageChange: delta.previousVotingPower + ? delta.percentageChange.toString() + : PERCENTAGE_NO_BASELINE, +}); + export const VotingPowerVariationsMapper = ( variations: DBVotingPowerVariation[], endTimestamp: number, days: DaysEnum, ): VotingPowerVariationsResponse => { return VotingPowerVariationsResponseSchema.parse({ - period: { - days: DaysEnum[days] as string, - startTimestamp: new Date((endTimestamp - days) * 1000).toISOString(), - endTimestamp: new Date(endTimestamp * 1000).toISOString(), - }, - items: variations.map( - ({ - accountId, - previousVotingPower, - currentVotingPower, - absoluteChange, - percentageChange, - }) => ({ - accountId: accountId, - previousVotingPower: previousVotingPower?.toString(), - currentVotingPower: currentVotingPower.toString(), - absoluteChange: absoluteChange.toString(), - percentageChange: previousVotingPower - ? percentageChange.toString() - : PERCENTAGE_NO_BASELINE, - }), - ), + period: PeriodResponseMapper(endTimestamp, days), + items: variations.map(VotingPowerVariationResponseMapper), + }); +}; + +export const VotingPowerVariationsByAccountIdMapper = ( + delta: DBVotingPowerVariation, + endTimestamp: number, + days: DaysEnum, +): VotingPowerVariationsByAccountIdResponse => { + return VotingPowerVariationsByAccountIdResponseSchema.parse({ + period: PeriodResponseMapper(endTimestamp, days), + data: VotingPowerVariationResponseMapper(delta), }); }; diff --git a/apps/indexer/src/api/repositories/voting-power/general.ts b/apps/indexer/src/api/repositories/voting-power/general.ts index afbfaf93a..97899641e 100644 --- a/apps/indexer/src/api/repositories/voting-power/general.ts +++ b/apps/indexer/src/api/repositories/voting-power/general.ts @@ -129,7 +129,7 @@ export class VotingPowerRepository { })); } - async getVotingPowerChanges( + async getVotingPowerVariations( startTimestamp: number, limit: number, skip: number, @@ -183,4 +183,50 @@ export class VotingPowerRepository { }; }); } + + async getVotingPowerVariationsByAccountId( + accountId: Address, + startTimestamp: number, + ): Promise { + const [delta] = await db + .select({ + accountId: votingPowerHistory.accountId, + absoluteChange: sql`SUM(${votingPowerHistory.delta})`.as( + "agg_delta", + ), + }) + .from(votingPowerHistory) + .orderBy(desc(votingPowerHistory.timestamp)) + .groupBy(votingPowerHistory.accountId, votingPowerHistory.timestamp) + .where( + and( + eq(votingPowerHistory.accountId, accountId), + gte(votingPowerHistory.timestamp, BigInt(startTimestamp)), + ), + ); + + const [currentAccountPower] = await db + .select({ currentVotingPower: accountPower.votingPower }) + .from(accountPower) + .where(eq(accountPower.accountId, accountId)); + + if (!(delta && currentAccountPower)) { + throw new Error(`Account not found`); + } + + const numericAbsoluteChange = BigInt(delta!.absoluteChange); + const currentVotingPower = currentAccountPower.currentVotingPower; + const oldVotingPower = currentVotingPower - numericAbsoluteChange; + const percentageChange = oldVotingPower + ? Number((numericAbsoluteChange * 10000n) / oldVotingPower) / 100 + : 0; + + return { + accountId: accountId, + previousVotingPower: currentVotingPower - numericAbsoluteChange, + currentVotingPower: currentVotingPower, + absoluteChange: numericAbsoluteChange, + percentageChange: percentageChange, + }; + } } diff --git a/apps/indexer/src/api/services/voting-power/voting-power.ts b/apps/indexer/src/api/services/voting-power/voting-power.ts index f2ae28ce1..914f6dcf5 100644 --- a/apps/indexer/src/api/services/voting-power/voting-power.ts +++ b/apps/indexer/src/api/services/voting-power/voting-power.ts @@ -24,12 +24,17 @@ interface VotingPowerRepository { } interface VotingPowerVariationRepository { - getVotingPowerChanges( + getVotingPowerVariations( startTimestamp: number, limit: number, skip: number, orderDirection: "asc" | "desc", ): Promise; + + getVotingPowerVariationsByAccountId( + accountId: Address, + startTimestamp: number, + ): Promise; } export class VotingPowerService { @@ -71,11 +76,21 @@ export class VotingPowerService { limit: number, orderDirection: "asc" | "desc", ): Promise { - return this.votingPowerVariationRepository.getVotingPowerChanges( + return this.votingPowerVariationRepository.getVotingPowerVariations( startTimestamp, limit, skip, orderDirection, ); } + + async getVotingPowerVariationsByAccountId( + accountId: Address, + startTimestamp: number, + ): Promise { + return this.votingPowerVariationRepository.getVotingPowerVariationsByAccountId( + accountId, + startTimestamp, + ); + } } From 7a1b862501fd6a6e615567dada43d87c17525ad6 Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Wed, 17 Dec 2025 11:51:37 -0300 Subject: [PATCH 2/8] chore(power-gql-to-rest): work in progress --- apps/api-gateway/schema.graphql | 107 +++++++++--------- apps/api-gateway/src/resolvers/rest.ts | 2 +- .../account-balance/historical-balances.ts | 60 ---------- .../{voting-powers.ts => historical.ts} | 14 +-- .../src/api/controllers/voting-power/index.ts | 4 +- .../api/controllers/voting-power/listing.ts | 0 ...ting-power-variations.ts => variations.ts} | 5 +- .../api/mappers/voting-power/voting-power.ts | 28 +++-- .../api/repositories/voting-power/general.ts | 53 ++++----- .../api/repositories/voting-power/nouns.ts | 2 +- .../voting-power/historical-voting-power.ts | 30 ----- .../src/api/services/voting-power/index.ts | 98 +++++++++++++++- .../api/services/voting-power/voting-power.ts | 96 ---------------- 13 files changed, 199 insertions(+), 300 deletions(-) rename apps/indexer/src/api/controllers/voting-power/{voting-powers.ts => historical.ts} (75%) create mode 100644 apps/indexer/src/api/controllers/voting-power/listing.ts rename apps/indexer/src/api/controllers/voting-power/{voting-power-variations.ts => variations.ts} (96%) delete mode 100644 apps/indexer/src/api/services/voting-power/historical-voting-power.ts delete mode 100644 apps/indexer/src/api/services/voting-power/voting-power.ts diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 5841a3119..015375e27 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -11,7 +11,7 @@ directive @httpOperation(subgraph: String, path: String, operationSpecificHeader directive @transport(subgraph: String, kind: String, location: String, headers: [[String]], queryStringOptions: ObjMap, queryParams: [[String]]) repeatable on SCHEMA type Query { - """Get property data for a specific token""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/token`\nGet property data for a specific token\n" token(currency: queryInput_token_currency = usd): token_200_response tokens(where: tokenFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): tokenPage! account(id: String!): account @@ -20,7 +20,7 @@ type Query { accountBalances(where: accountBalanceFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): accountBalancePage! accountPower(accountId: String!): accountPower accountPowers(where: accountPowerFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): accountPowerPage! - votingPowerHistory(transactionHash: String!, accountId: String!): votingPowerHistory + votingPowerHistory(transactionHash: String!, accountId: String!, logIndex: Float!): votingPowerHistory votingPowerHistorys(where: votingPowerHistoryFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): votingPowerHistoryPage! delegation(transactionHash: String!, delegatorAccountId: String!, delegateAccountId: String!): delegation delegations(where: delegationFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): delegationPage! @@ -38,72 +38,64 @@ type Query { tokenPrices(where: tokenPriceFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): tokenPricePage! _meta: Meta - """Get total assets""" + "\n>**Method**: `GET`\n>**Base URL**: `https://ens-api-dev-0561.up.railway.app`\n>**Path**: `/total-assets`\nGet total assets\n" totalAssets(days: queryInput_totalAssets_days = _7d): [query_totalAssets_items] - """Get historical market data for a specific token""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/token/historical-data`\nGet historical market data for a specific token\n" historicalTokenData(skip: NonNegativeInt, limit: Float = 365): [query_historicalTokenData_items] - """Compare total supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/total-supply/compare`\nCompare total supply between periods\n" compareTotalSupply(days: queryInput_compareTotalSupply_days = _90d): compareTotalSupply_200_response - """Compare delegated supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/delegated-supply/compare`\nCompare delegated supply between periods\n" compareDelegatedSupply(days: queryInput_compareDelegatedSupply_days = _90d): compareDelegatedSupply_200_response - """Compare circulating supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/circulating-supply/compare`\nCompare circulating supply between periods\n" compareCirculatingSupply(days: queryInput_compareCirculatingSupply_days = _90d): compareCirculatingSupply_200_response - """Compare treasury between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/treasury/compare`\nCompare treasury between periods\n" compareTreasury(days: queryInput_compareTreasury_days = _90d): compareTreasury_200_response - """Compare cex supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/cex-supply/compare`\nCompare cex supply between periods\n" compareCexSupply(days: queryInput_compareCexSupply_days = _90d): compareCexSupply_200_response - """Compare dex supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/dex-supply/compare`\nCompare dex supply between periods\n" compareDexSupply(days: queryInput_compareDexSupply_days = _90d): compareDexSupply_200_response - """Compare lending supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/lending-supply/compare`\nCompare lending supply between periods\n" compareLendingSupply(days: queryInput_compareLendingSupply_days = _90d): compareLendingSupply_200_response - """Get active token supply for DAO""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/active-supply/compare`\nGet active token supply for DAO\n" compareActiveSupply(days: queryInput_compareActiveSupply_days = _90d): compareActiveSupply_200_response - """Compare number of proposals between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals/compare`\nCompare number of proposals between time periods\n" compareProposals(days: queryInput_compareProposals_days = _90d): compareProposals_200_response - """Compare number of votes between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/votes/compare`\nCompare number of votes between time periods\n" compareVotes(days: queryInput_compareVotes_days = _90d): compareVotes_200_response - """Compare average turnout between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/average-turnout/compare`\nCompare average turnout between time periods\n" compareAverageTurnout(days: queryInput_compareAverageTurnout_days = _90d): compareAverageTurnout_200_response - """ - Returns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window - """ + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals-activity`\nReturns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window\n" proposalsActivity(address: String!, fromDate: NonNegativeInt, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_proposalsActivity_orderBy = timestamp, orderDirection: queryInput_proposalsActivity_orderDirection = desc, userVoteFilter: queryInput_proposalsActivity_userVoteFilter): proposalsActivity_200_response - """Returns a list of proposal""" - proposals(skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposals_orderDirection = desc, status: JSON, fromDate: Float, fromEndDate: Float): proposals_200_response + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals`\nReturns a list of proposal\n" + proposals(skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposals_orderDirection = desc, status: JSON, fromDate: Float, fromEndDate: Float, includeOptimisticProposals: queryInput_proposals_includeOptimisticProposals = TRUE): proposals_200_response - """Returns a single proposal by its ID""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals/{args.id}`\nReturns a single proposal by its ID\n" proposal(id: String!): proposal_200_response - """Returns the active delegates that did not vote on a given proposal""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals/{args.id}/non-voters`\nReturns the active delegates that did not vote on a given proposal\n" proposalNonVoters(id: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposalNonVoters_orderDirection = desc, addresses: JSON): proposalNonVoters_200_response - """ - Fetch historical token balances for multiple addresses at a specific time period using multicall - """ + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/historical-balances`\nFetch historical token balances for multiple addresses at a specific time period using multicall\n" historicalBalances(addresses: JSON!, days: queryInput_historicalBalances_days = _7d): [query_historicalBalances_items] - """ - Fetch historical voting power for multiple addresses at a specific time period using multicall - """ + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/historical-voting-power`\nFetch historical voting power for multiple addresses at a specific time period using multicall\n" historicalVotingPower(addresses: JSON!, days: queryInput_historicalVotingPower_days = _7d, fromDate: Float): [query_historicalVotingPower_items] - """ - Get transactions with their associated transfers and delegations, with optional filtering and sorting - """ + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/transactions`\nGet transactions with their associated transfers and delegations, with optional filtering and sorting\n" transactions( limit: PositiveInt = 50 offset: NonNegativeInt @@ -125,32 +117,25 @@ type Query { includes: JSON ): transactions_200_response - """Get the last update time""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/last-update`\nGet the last update time\n" lastUpdate(chart: queryInput_lastUpdate_chart!): lastUpdate_200_response - """Get delegation percentage day buckets with forward-fill""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/delegation-percentage`\nGet delegation percentage day buckets with forward-fill\n" delegationPercentageByDay(startDate: String, endDate: String, orderDirection: queryInput_delegationPercentageByDay_orderDirection = asc, limit: NonNegativeInt = 365, after: String, before: String): delegationPercentageByDay_200_response - """Returns a list of voting power changes""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/voting-powers`\nReturns a list of voting power changes\n" votingPowers(account: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_votingPowers_orderBy = timestamp, orderDirection: queryInput_votingPowers_orderDirection = desc, minDelta: String, maxDelta: String): votingPowers_200_response - """ - Returns a mapping of the biggest changes to voting power associated by delegate address - """ + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/voting-power/variations`\nReturns a mapping of the biggest changes to voting power associated by delegate address\n" votingPowerVariations(days: queryInput_votingPowerVariations_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_votingPowerVariations_orderDirection = desc): votingPowerVariations_200_response - """ - Returns a mapping of the biggest variations to account balances associated by account address - """ + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/account-balance/variations`\nReturns a mapping of the biggest variations to account balances associated by account address\n" accountBalanceVariations(days: queryInput_accountBalanceVariations_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountBalanceVariations_orderDirection = desc): accountBalanceVariations_200_response - """ - Returns a mapping of the largest interactions between accounts. - Positive amounts signify net token transfers FROM , whilst negative amounts refer to net transfers TO - """ - accountInteractions(days: queryInput_accountInteractions_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountInteractions_orderDirection = desc, accountId: String!, minAmount: String, maxAmount: String): accountInteractions_200_response + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/account-balance/interactions`\nReturns a mapping of the largest interactions between accounts. \nPositive amounts signify net token transfers FROM , whilst negative amounts refer to net transfers TO \n" + accountInteractions(days: queryInput_accountInteractions_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountInteractions_orderDirection = desc, accountId: String!, minAmount: String, maxAmount: String, orderBy: queryInput_accountInteractions_orderBy = count, address: String): accountInteractions_200_response - """Returns current governance parameters for this DAO""" + "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/dao`\nReturns current governance parameters for this DAO\n" dao: dao_200_response """ @@ -358,7 +343,6 @@ type accountPower { proposalsCount: Int! delegationsCount: Int! lastVoteTimestamp: BigInt! - firstVoteTimestamp: BigInt account: account } @@ -474,14 +458,6 @@ input accountPowerFilter { lastVoteTimestamp_lt: BigInt lastVoteTimestamp_gte: BigInt lastVoteTimestamp_lte: BigInt - firstVoteTimestamp: BigInt - firstVoteTimestamp_not: BigInt - firstVoteTimestamp_in: [BigInt] - firstVoteTimestamp_not_in: [BigInt] - firstVoteTimestamp_gt: BigInt - firstVoteTimestamp_lt: BigInt - firstVoteTimestamp_gte: BigInt - firstVoteTimestamp_lte: BigInt } type delegationPage { @@ -760,6 +736,7 @@ type proposalsOnchain { forVotes: BigInt! againstVotes: BigInt! abstainVotes: BigInt! + proposalType: Int votes(where: votesOnchainFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): votesOnchainPage proposer: account } @@ -983,6 +960,14 @@ input proposalsOnchainFilter { abstainVotes_lt: BigInt abstainVotes_gte: BigInt abstainVotes_lte: BigInt + proposalType: Int + proposalType_not: Int + proposalType_in: [Int] + proposalType_not_in: [Int] + proposalType_gt: Int + proposalType_lt: Int + proposalType_gte: Int + proposalType_lte: Int } type accountPage { @@ -1611,6 +1596,7 @@ type query_proposals_items_items { calldatas: [String]! values: [String]! targets: [String]! + proposalType: Float } enum queryInput_proposals_orderDirection { @@ -1618,6 +1604,11 @@ enum queryInput_proposals_orderDirection { desc } +enum queryInput_proposals_includeOptimisticProposals { + TRUE + FALSE +} + type proposal_200_response { id: String! daoId: String! @@ -1638,6 +1629,7 @@ type proposal_200_response { calldatas: [String]! values: [String]! targets: [String]! + proposalType: Float } type proposalNonVoters_200_response { @@ -1909,6 +1901,11 @@ enum queryInput_accountInteractions_orderDirection { desc } +enum queryInput_accountInteractions_orderBy { + volume + count +} + type dao_200_response { id: String! chainId: Float! diff --git a/apps/api-gateway/src/resolvers/rest.ts b/apps/api-gateway/src/resolvers/rest.ts index 0e832aec1..f5e98671a 100644 --- a/apps/api-gateway/src/resolvers/rest.ts +++ b/apps/api-gateway/src/resolvers/rest.ts @@ -16,7 +16,6 @@ const daoItemQueries = [ "historicalTokenData", "proposalsActivity", "historicalBalances", - "historicalVotingPower", "proposals", "transactions", "lastUpdate", @@ -25,6 +24,7 @@ const daoItemQueries = [ "proposalNonVoters", "token", "votingPowerVariations", + "votingPowerVariationsByAccountId", "accountBalanceVariations", "delegationPercentageByDay", "dao", diff --git a/apps/indexer/src/api/controllers/account-balance/historical-balances.ts b/apps/indexer/src/api/controllers/account-balance/historical-balances.ts index f33a9500f..17a781b2c 100644 --- a/apps/indexer/src/api/controllers/account-balance/historical-balances.ts +++ b/apps/indexer/src/api/controllers/account-balance/historical-balances.ts @@ -82,64 +82,4 @@ export function historicalBalances( ); }, ); - - // Historical Voting Power endpoint - app.openapi( - createRoute({ - method: "get", - operationId: "historicalVotingPower", - path: "/historical-voting-power", - summary: "Get historical voting power", - description: - "Fetch historical voting power for multiple addresses at a specific time period using multicall", - tags: ["historical-onchain"], - request: { - query: z.object({ - addresses: z - .array(z.string()) - .min(1, "At least one address is required") - .refine((addresses) => - addresses.every((address) => isAddress(address)), - ) - .or( - z - .string() - .refine((addr) => isAddress(addr), "Invalid Ethereum address") - .transform((addr) => [addr]), - ), - days: z - .enum(DaysOpts) - .default("7d") - .transform((val) => DaysEnum[val]), - fromDate: z.coerce.number().optional(), - }), - }, - responses: { - 200: { - description: "Successfully retrieved historical voting power", - content: { - "application/json": { - schema: z.array( - z.object({ - address: z.string(), - votingPower: z.string(), - }), - ), - }, - }, - }, - }, - }), - async (context) => { - const { addresses, days, fromDate } = context.req.valid("query"); - - const votingPowers = await votingPowerService.getHistoricalVotingPower( - addresses, - days, - fromDate || Math.floor(Date.now() / 1000), - ); - - return context.json(votingPowers); - }, - ); } diff --git a/apps/indexer/src/api/controllers/voting-power/voting-powers.ts b/apps/indexer/src/api/controllers/voting-power/historical.ts similarity index 75% rename from apps/indexer/src/api/controllers/voting-power/voting-powers.ts rename to apps/indexer/src/api/controllers/voting-power/historical.ts index dd7430722..a98137765 100644 --- a/apps/indexer/src/api/controllers/voting-power/voting-powers.ts +++ b/apps/indexer/src/api/controllers/voting-power/historical.ts @@ -2,8 +2,8 @@ import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; import { VotingPowerService } from "@/api/services"; import { - VotingPowerResponseSchema, - VotingPowerRequestSchema, + HistoricalVotingPowerResponseSchema, + HistoricalVotingPowerRequestSchema, VotingPowerMapper, } from "@/api/mappers"; @@ -11,20 +11,20 @@ export function votingPower(app: Hono, service: VotingPowerService) { app.openapi( createRoute({ method: "get", - operationId: "votingPowers", - path: "/voting-powers", + operationId: "historicalVotingPowers", + path: "/voting-powers/historical", summary: "Get voting power changes", description: "Returns a list of voting power changes", tags: ["proposals"], request: { - query: VotingPowerRequestSchema, + query: HistoricalVotingPowerRequestSchema, }, responses: { 200: { description: "Successfully retrieved voting power changes", content: { "application/json": { - schema: VotingPowerResponseSchema, + schema: HistoricalVotingPowerResponseSchema, }, }, }, @@ -41,7 +41,7 @@ export function votingPower(app: Hono, service: VotingPowerService) { maxDelta, } = context.req.valid("query"); - const { items, totalCount } = await service.getVotingPowers( + const { items, totalCount } = await service.getHistoricalVotingPowers( account, skip, limit, diff --git a/apps/indexer/src/api/controllers/voting-power/index.ts b/apps/indexer/src/api/controllers/voting-power/index.ts index 0e12f3c12..cb880d8c5 100644 --- a/apps/indexer/src/api/controllers/voting-power/index.ts +++ b/apps/indexer/src/api/controllers/voting-power/index.ts @@ -1,2 +1,2 @@ -export * from "./voting-powers"; -export * from "./voting-power-variations"; +export * from "./historical"; +export * from "./variations"; diff --git a/apps/indexer/src/api/controllers/voting-power/listing.ts b/apps/indexer/src/api/controllers/voting-power/listing.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts b/apps/indexer/src/api/controllers/voting-power/variations.ts similarity index 96% rename from apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts rename to apps/indexer/src/api/controllers/voting-power/variations.ts index 63caea71d..5a023f63a 100644 --- a/apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts +++ b/apps/indexer/src/api/controllers/voting-power/variations.ts @@ -6,6 +6,7 @@ import { VotingPowerVariationsByAccountIdMapper, VotingPowerVariationsRequestSchema, VotingPowerVariationsResponseSchema, + VotingPowerVariationsMapper, } from "@/api/mappers/"; import { Address } from "viem"; @@ -44,9 +45,7 @@ export function votingPowerVariations(app: Hono, service: VotingPowerService) { orderDirection, ); - return context.json( - VotingPowerVariationsByAccountIdMapper(result, now, days), - ); + return context.json(VotingPowerVariationsMapper(result, now, days)); }, ); diff --git a/apps/indexer/src/api/mappers/voting-power/voting-power.ts b/apps/indexer/src/api/mappers/voting-power/voting-power.ts index d966fa781..7431d6727 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power.ts @@ -10,7 +10,7 @@ export type DBVotingPowerWithRelations = DBVotingPower & { transfers: DBTransfer | null; }; -export const VotingPowerRequestSchema = z.object({ +export const HistoricalVotingPowerRequestSchema = z.object({ account: z.string().refine((addr) => isAddress(addr)), skip: z.coerce .number() @@ -31,9 +31,11 @@ export const VotingPowerRequestSchema = z.object({ maxDelta: z.string().optional(), }); -export type VotingPowerRequest = z.infer; +export type VotingPowerRequest = z.infer< + typeof HistoricalVotingPowerRequestSchema +>; -export const VotingPowerResponseSchema = z.object({ +export const HistoricalVotingPowerResponseSchema = z.object({ items: z.array( z.object({ transactionHash: z.string(), @@ -62,7 +64,9 @@ export const VotingPowerResponseSchema = z.object({ totalCount: z.number(), }); -export type VotingPowerResponse = z.infer; +export type VotingPowerResponse = z.infer< + typeof HistoricalVotingPowerResponseSchema +>; export const VotingPowerMapper = ( p: DBVotingPowerWithRelations[], @@ -79,17 +83,17 @@ export const VotingPowerMapper = ( logIndex: p.logIndex, delegation: p.delegations ? { - from: p.delegations.delegatorAccountId, - value: p.delegations.delegatedValue.toString(), - to: p.delegations.delegateAccountId, - } + from: p.delegations.delegatorAccountId, + value: p.delegations.delegatedValue.toString(), + to: p.delegations.delegateAccountId, + } : null, transfer: p.transfers ? { - value: p.transfers.amount.toString(), - from: p.transfers.fromAccountId, - to: p.transfers.toAccountId, - } + value: p.transfers.amount.toString(), + from: p.transfers.fromAccountId, + to: p.transfers.toAccountId, + } : null, })), totalCount, diff --git a/apps/indexer/src/api/repositories/voting-power/general.ts b/apps/indexer/src/api/repositories/voting-power/general.ts index 97899641e..e8166d656 100644 --- a/apps/indexer/src/api/repositories/voting-power/general.ts +++ b/apps/indexer/src/api/repositories/voting-power/general.ts @@ -1,5 +1,5 @@ import { Address } from "viem"; -import { gte, and, inArray, lte, desc, eq, asc, sql } from "drizzle-orm"; +import { gte, and, lte, desc, eq, asc, sql } from "drizzle-orm"; import { db } from "ponder:api"; import { votingPowerHistory, @@ -14,28 +14,6 @@ import { } from "@/api/mappers"; export class VotingPowerRepository { - async getHistoricalVotingPower( - addresses: Address[], - timestamp: bigint, - ): Promise<{ address: Address; votingPower: bigint }[]> { - return await db - .selectDistinctOn([votingPowerHistory.accountId], { - address: votingPowerHistory.accountId, - votingPower: votingPowerHistory.votingPower, - }) - .from(votingPowerHistory) - .where( - and( - inArray(votingPowerHistory.accountId, addresses), - lte(votingPowerHistory.timestamp, timestamp), - ), - ) - .orderBy( - votingPowerHistory.accountId, - desc(votingPowerHistory.timestamp), - ); - } - async getVotingPowerCount( accountId: Address, minDelta?: string, @@ -55,7 +33,7 @@ export class VotingPowerRepository { ); } - async getVotingPowers( + async getHistoricalVotingPowers( accountId: Address, skip: number, limit: number, @@ -188,22 +166,28 @@ export class VotingPowerRepository { accountId: Address, startTimestamp: number, ): Promise { - const [delta] = await db + const history = db .select({ accountId: votingPowerHistory.accountId, - absoluteChange: sql`SUM(${votingPowerHistory.delta})`.as( - "agg_delta", - ), + delta: votingPowerHistory.delta, }) .from(votingPowerHistory) .orderBy(desc(votingPowerHistory.timestamp)) - .groupBy(votingPowerHistory.accountId, votingPowerHistory.timestamp) .where( and( eq(votingPowerHistory.accountId, accountId), gte(votingPowerHistory.timestamp, BigInt(startTimestamp)), ), - ); + ) + .as("history"); + + const [delta] = await db + .select({ + accountId: history.accountId, + absoluteChange: sql`SUM(${history.delta})`.as("agg_delta"), + }) + .from(history) + .groupBy(history.accountId); const [currentAccountPower] = await db .select({ currentVotingPower: accountPower.votingPower }) @@ -211,12 +195,19 @@ export class VotingPowerRepository { .where(eq(accountPower.accountId, accountId)); if (!(delta && currentAccountPower)) { - throw new Error(`Account not found`); + throw new Error("Account not found"); } const numericAbsoluteChange = BigInt(delta!.absoluteChange); const currentVotingPower = currentAccountPower.currentVotingPower; const oldVotingPower = currentVotingPower - numericAbsoluteChange; + console.log({ + numericAbsoluteChange: numericAbsoluteChange, + currentVotingPower: currentVotingPower, + oldVotingPower: oldVotingPower, + percentageChange: + Number((numericAbsoluteChange * 10000n) / oldVotingPower) / 100, + }); const percentageChange = oldVotingPower ? Number((numericAbsoluteChange * 10000n) / oldVotingPower) / 100 : 0; diff --git a/apps/indexer/src/api/repositories/voting-power/nouns.ts b/apps/indexer/src/api/repositories/voting-power/nouns.ts index 9817d0eea..2a547dcd2 100644 --- a/apps/indexer/src/api/repositories/voting-power/nouns.ts +++ b/apps/indexer/src/api/repositories/voting-power/nouns.ts @@ -25,7 +25,7 @@ export class NounsVotingPowerRepository { ); } - async getVotingPowers( + async getHistoricalVotingPowers( accountId: Address, skip: number, limit: number, diff --git a/apps/indexer/src/api/services/voting-power/historical-voting-power.ts b/apps/indexer/src/api/services/voting-power/historical-voting-power.ts deleted file mode 100644 index 2f7197f4a..000000000 --- a/apps/indexer/src/api/services/voting-power/historical-voting-power.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Address } from "viem"; - -import { DaysEnum } from "@/lib/enums"; - -export interface HistoricalVotingPower { - address: Address; - votingPower: bigint; -} - -interface VotingPowerRepository { - getHistoricalVotingPower( - addresses: Address[], - timestamp: bigint, - ): Promise; -} - -export class HistoricalVotingPowerService { - constructor(private readonly repository: VotingPowerRepository) {} - - async getHistoricalVotingPower( - addresses: Address[], - daysInSeconds: DaysEnum, - fromDate: number, - ): Promise { - return await this.repository.getHistoricalVotingPower( - addresses, - BigInt(fromDate - daysInSeconds), - ); - } -} diff --git a/apps/indexer/src/api/services/voting-power/index.ts b/apps/indexer/src/api/services/voting-power/index.ts index d9cd0f812..2fd84e7e2 100644 --- a/apps/indexer/src/api/services/voting-power/index.ts +++ b/apps/indexer/src/api/services/voting-power/index.ts @@ -1,2 +1,96 @@ -export * from "./historical-voting-power"; -export * from "./voting-power"; +import { Address } from "viem"; + +import { + DBVotingPowerWithRelations, + DBVotingPowerVariation, +} from "@/api/mappers"; + +interface VotingPowerRepository { + getHistoricalVotingPowers( + accountId: Address, + skip: number, + limit: number, + orderDirection: "asc" | "desc", + orderBy: "timestamp" | "delta", + minDelta?: string, + maxDelta?: string, + ): Promise; + + getVotingPowerCount( + account: Address, + minDelta?: string, + maxDelta?: string, + ): Promise; +} + +interface VotingPowerVariationRepository { + getVotingPowerVariations( + startTimestamp: number, + limit: number, + skip: number, + orderDirection: "asc" | "desc", + ): Promise; + + getVotingPowerVariationsByAccountId( + accountId: Address, + startTimestamp: number, + ): Promise; +} + +export class VotingPowerService { + constructor( + private readonly votingRepository: VotingPowerRepository, + private readonly votingPowerVariationRepository: VotingPowerVariationRepository, + ) {} + + async getHistoricalVotingPowers( + account: Address, + skip: number, + limit: number, + orderDirection: "asc" | "desc" = "desc", + orderBy: "timestamp" | "delta" = "timestamp", + minDelta?: string, + maxDelta?: string, + ): Promise<{ items: DBVotingPowerWithRelations[]; totalCount: number }> { + const items = await this.votingRepository.getHistoricalVotingPowers( + account, + skip, + limit, + orderDirection, + orderBy, + minDelta, + maxDelta, + ); + + const totalCount = await this.votingRepository.getVotingPowerCount( + account, + minDelta, + maxDelta, + ); + return { items, totalCount }; + } + + async getVotingPowerVariations( + startTimestamp: number, + skip: number, + limit: number, + orderDirection: "asc" | "desc", + ): Promise { + return this.votingPowerVariationRepository.getVotingPowerVariations( + startTimestamp, + limit, + skip, + orderDirection, + ); + } + + async getVotingPowerVariationsByAccountId( + accountId: Address, + startTimestamp: number, + ): Promise { + return this.votingPowerVariationRepository.getVotingPowerVariationsByAccountId( + accountId, + startTimestamp, + ); + } +} diff --git a/apps/indexer/src/api/services/voting-power/voting-power.ts b/apps/indexer/src/api/services/voting-power/voting-power.ts deleted file mode 100644 index 914f6dcf5..000000000 --- a/apps/indexer/src/api/services/voting-power/voting-power.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Address } from "viem"; - -import { - DBVotingPowerWithRelations, - DBVotingPowerVariation, -} from "@/api/mappers"; - -interface VotingPowerRepository { - getVotingPowers( - accountId: Address, - skip: number, - limit: number, - orderDirection: "asc" | "desc", - orderBy: "timestamp" | "delta", - minDelta?: string, - maxDelta?: string, - ): Promise; - - getVotingPowerCount( - account: Address, - minDelta?: string, - maxDelta?: string, - ): Promise; -} - -interface VotingPowerVariationRepository { - getVotingPowerVariations( - startTimestamp: number, - limit: number, - skip: number, - orderDirection: "asc" | "desc", - ): Promise; - - getVotingPowerVariationsByAccountId( - accountId: Address, - startTimestamp: number, - ): Promise; -} - -export class VotingPowerService { - constructor( - private readonly votingRepository: VotingPowerRepository, - private readonly votingPowerVariationRepository: VotingPowerVariationRepository, - ) {} - - async getVotingPowers( - account: Address, - skip: number, - limit: number, - orderDirection: "asc" | "desc" = "desc", - orderBy: "timestamp" | "delta" = "timestamp", - minDelta?: string, - maxDelta?: string, - ): Promise<{ items: DBVotingPowerWithRelations[]; totalCount: number }> { - const items = await this.votingRepository.getVotingPowers( - account, - skip, - limit, - orderDirection, - orderBy, - minDelta, - maxDelta, - ); - - const totalCount = await this.votingRepository.getVotingPowerCount( - account, - minDelta, - maxDelta, - ); - return { items, totalCount }; - } - - async getVotingPowerVariations( - startTimestamp: number, - skip: number, - limit: number, - orderDirection: "asc" | "desc", - ): Promise { - return this.votingPowerVariationRepository.getVotingPowerVariations( - startTimestamp, - limit, - skip, - orderDirection, - ); - } - - async getVotingPowerVariationsByAccountId( - accountId: Address, - startTimestamp: number, - ): Promise { - return this.votingPowerVariationRepository.getVotingPowerVariationsByAccountId( - accountId, - startTimestamp, - ); - } -} From b3ba103fedc88d4ec886dfc9df6e4aa40243375e Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Wed, 17 Dec 2025 20:16:54 -0300 Subject: [PATCH 3/8] chore(power-gql-to-rest): scaffold complete --- apps/api-gateway/schema.graphql | 71 +++++++++------ apps/api-gateway/src/resolvers/rest.ts | 2 +- .../account-balance/historical-balances.ts | 6 +- .../controllers/voting-power/historical.ts | 4 +- .../api/controllers/voting-power/listing.ts | 90 +++++++++++++++++++ .../controllers/voting-power/variations.ts | 11 ++- apps/indexer/src/api/index.ts | 8 +- .../voting-power/voting-power-variations.ts | 77 ++++++++++++++++ .../api/mappers/voting-power/voting-power.ts | 14 +-- .../api/repositories/voting-power/general.ts | 89 +++++++++++++++++- .../api/repositories/voting-power/nouns.ts | 6 +- .../src/api/services/voting-power/index.ts | 87 +++++++++++++----- 12 files changed, 382 insertions(+), 83 deletions(-) diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 015375e27..39bbe6f9e 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -11,7 +11,7 @@ directive @httpOperation(subgraph: String, path: String, operationSpecificHeader directive @transport(subgraph: String, kind: String, location: String, headers: [[String]], queryStringOptions: ObjMap, queryParams: [[String]]) repeatable on SCHEMA type Query { - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/token`\nGet property data for a specific token\n" + """Get property data for a specific token""" token(currency: queryInput_token_currency = usd): token_200_response tokens(where: tokenFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): tokenPage! account(id: String!): account @@ -38,64 +38,72 @@ type Query { tokenPrices(where: tokenPriceFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): tokenPricePage! _meta: Meta - "\n>**Method**: `GET`\n>**Base URL**: `https://ens-api-dev-0561.up.railway.app`\n>**Path**: `/total-assets`\nGet total assets\n" + """Get total assets""" totalAssets(days: queryInput_totalAssets_days = _7d): [query_totalAssets_items] - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/token/historical-data`\nGet historical market data for a specific token\n" + """Get historical market data for a specific token""" historicalTokenData(skip: NonNegativeInt, limit: Float = 365): [query_historicalTokenData_items] - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/total-supply/compare`\nCompare total supply between periods\n" + """Compare total supply between periods""" compareTotalSupply(days: queryInput_compareTotalSupply_days = _90d): compareTotalSupply_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/delegated-supply/compare`\nCompare delegated supply between periods\n" + """Compare delegated supply between periods""" compareDelegatedSupply(days: queryInput_compareDelegatedSupply_days = _90d): compareDelegatedSupply_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/circulating-supply/compare`\nCompare circulating supply between periods\n" + """Compare circulating supply between periods""" compareCirculatingSupply(days: queryInput_compareCirculatingSupply_days = _90d): compareCirculatingSupply_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/treasury/compare`\nCompare treasury between periods\n" + """Compare treasury between periods""" compareTreasury(days: queryInput_compareTreasury_days = _90d): compareTreasury_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/cex-supply/compare`\nCompare cex supply between periods\n" + """Compare cex supply between periods""" compareCexSupply(days: queryInput_compareCexSupply_days = _90d): compareCexSupply_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/dex-supply/compare`\nCompare dex supply between periods\n" + """Compare dex supply between periods""" compareDexSupply(days: queryInput_compareDexSupply_days = _90d): compareDexSupply_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/lending-supply/compare`\nCompare lending supply between periods\n" + """Compare lending supply between periods""" compareLendingSupply(days: queryInput_compareLendingSupply_days = _90d): compareLendingSupply_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/active-supply/compare`\nGet active token supply for DAO\n" + """Get active token supply for DAO""" compareActiveSupply(days: queryInput_compareActiveSupply_days = _90d): compareActiveSupply_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals/compare`\nCompare number of proposals between time periods\n" + """Compare number of proposals between time periods""" compareProposals(days: queryInput_compareProposals_days = _90d): compareProposals_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/votes/compare`\nCompare number of votes between time periods\n" + """Compare number of votes between time periods""" compareVotes(days: queryInput_compareVotes_days = _90d): compareVotes_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/average-turnout/compare`\nCompare average turnout between time periods\n" + """Compare average turnout between time periods""" compareAverageTurnout(days: queryInput_compareAverageTurnout_days = _90d): compareAverageTurnout_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals-activity`\nReturns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window\n" + """ + Returns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window + """ proposalsActivity(address: String!, fromDate: NonNegativeInt, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_proposalsActivity_orderBy = timestamp, orderDirection: queryInput_proposalsActivity_orderDirection = desc, userVoteFilter: queryInput_proposalsActivity_userVoteFilter): proposalsActivity_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals`\nReturns a list of proposal\n" + """Returns a list of proposal""" proposals(skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposals_orderDirection = desc, status: JSON, fromDate: Float, fromEndDate: Float, includeOptimisticProposals: queryInput_proposals_includeOptimisticProposals = TRUE): proposals_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals/{args.id}`\nReturns a single proposal by its ID\n" + """Returns a single proposal by its ID""" proposal(id: String!): proposal_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/proposals/{args.id}/non-voters`\nReturns the active delegates that did not vote on a given proposal\n" + """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 - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/historical-balances`\nFetch historical token balances for multiple addresses at a specific time period using multicall\n" + """ + Fetch historical token balances for multiple addresses at a specific time period using multicall + """ historicalBalances(addresses: JSON!, days: queryInput_historicalBalances_days = _7d): [query_historicalBalances_items] - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/historical-voting-power`\nFetch historical voting power for multiple addresses at a specific time period using multicall\n" + """ + Fetch historical voting power for multiple addresses at a specific time period using multicall + """ historicalVotingPower(addresses: JSON!, days: queryInput_historicalVotingPower_days = _7d, fromDate: Float): [query_historicalVotingPower_items] - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/transactions`\nGet transactions with their associated transfers and delegations, with optional filtering and sorting\n" + """ + Get transactions with their associated transfers and delegations, with optional filtering and sorting + """ transactions( limit: PositiveInt = 50 offset: NonNegativeInt @@ -117,25 +125,32 @@ type Query { includes: JSON ): transactions_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/last-update`\nGet the last update time\n" + """Get the last update time""" lastUpdate(chart: queryInput_lastUpdate_chart!): lastUpdate_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/delegation-percentage`\nGet delegation percentage day buckets with forward-fill\n" + """Get delegation percentage day buckets with forward-fill""" delegationPercentageByDay(startDate: String, endDate: String, orderDirection: queryInput_delegationPercentageByDay_orderDirection = asc, limit: NonNegativeInt = 365, after: String, before: String): delegationPercentageByDay_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/voting-powers`\nReturns a list of voting power changes\n" + """Returns a list of voting power changes""" votingPowers(account: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_votingPowers_orderBy = timestamp, orderDirection: queryInput_votingPowers_orderDirection = desc, minDelta: String, maxDelta: String): votingPowers_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/voting-power/variations`\nReturns a mapping of the biggest changes to voting power associated by delegate address\n" + """ + Returns a mapping of the biggest changes to voting power associated by delegate address + """ votingPowerVariations(days: queryInput_votingPowerVariations_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_votingPowerVariations_orderDirection = desc): votingPowerVariations_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/account-balance/variations`\nReturns a mapping of the biggest variations to account balances associated by account address\n" + """ + Returns a mapping of the biggest variations to account balances associated by account address + """ accountBalanceVariations(days: queryInput_accountBalanceVariations_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountBalanceVariations_orderDirection = desc): accountBalanceVariations_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/account-balance/interactions`\nReturns a mapping of the largest interactions between accounts. \nPositive amounts signify net token transfers FROM , whilst negative amounts refer to net transfers TO \n" + """ + Returns a mapping of the largest interactions between accounts. + Positive amounts signify net token transfers FROM , whilst negative amounts refer to net transfers TO + """ accountInteractions(days: queryInput_accountInteractions_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountInteractions_orderDirection = desc, accountId: String!, minAmount: String, maxAmount: String, orderBy: queryInput_accountInteractions_orderBy = count, address: String): accountInteractions_200_response - "\n>**Method**: `GET`\n>**Base URL**: `https://obol-api-dev.up.railway.app`\n>**Path**: `/dao`\nReturns current governance parameters for this DAO\n" + """Returns current governance parameters for this DAO""" dao: dao_200_response """ diff --git a/apps/api-gateway/src/resolvers/rest.ts b/apps/api-gateway/src/resolvers/rest.ts index f5e98671a..0e832aec1 100644 --- a/apps/api-gateway/src/resolvers/rest.ts +++ b/apps/api-gateway/src/resolvers/rest.ts @@ -16,6 +16,7 @@ const daoItemQueries = [ "historicalTokenData", "proposalsActivity", "historicalBalances", + "historicalVotingPower", "proposals", "transactions", "lastUpdate", @@ -24,7 +25,6 @@ const daoItemQueries = [ "proposalNonVoters", "token", "votingPowerVariations", - "votingPowerVariationsByAccountId", "accountBalanceVariations", "delegationPercentageByDay", "dao", diff --git a/apps/indexer/src/api/controllers/account-balance/historical-balances.ts b/apps/indexer/src/api/controllers/account-balance/historical-balances.ts index 17a781b2c..68b94054e 100644 --- a/apps/indexer/src/api/controllers/account-balance/historical-balances.ts +++ b/apps/indexer/src/api/controllers/account-balance/historical-balances.ts @@ -2,16 +2,12 @@ import { isAddress } from "viem"; import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi"; import { DaysOpts, DaysEnum, DaoIdEnum } from "@/lib/enums"; -import { - HistoricalBalancesService, - HistoricalVotingPowerService, -} from "@/api/services"; +import { HistoricalBalancesService } from "@/api/services"; import { HistoricalBalanceMapper } from "@/api/mappers"; export function historicalBalances( app: Hono, daoId: DaoIdEnum, - votingPowerService: HistoricalVotingPowerService, balancesService: HistoricalBalancesService, ) { // Historical Balances endpoint diff --git a/apps/indexer/src/api/controllers/voting-power/historical.ts b/apps/indexer/src/api/controllers/voting-power/historical.ts index a98137765..d358d560e 100644 --- a/apps/indexer/src/api/controllers/voting-power/historical.ts +++ b/apps/indexer/src/api/controllers/voting-power/historical.ts @@ -4,7 +4,7 @@ import { VotingPowerService } from "@/api/services"; import { HistoricalVotingPowerResponseSchema, HistoricalVotingPowerRequestSchema, - VotingPowerMapper, + HistoricalVotingPowerMapper, } from "@/api/mappers"; export function votingPower(app: Hono, service: VotingPowerService) { @@ -50,7 +50,7 @@ export function votingPower(app: Hono, service: VotingPowerService) { minDelta, maxDelta, ); - return context.json(VotingPowerMapper(items, totalCount)); + return context.json(HistoricalVotingPowerMapper(items, totalCount)); }, ); } diff --git a/apps/indexer/src/api/controllers/voting-power/listing.ts b/apps/indexer/src/api/controllers/voting-power/listing.ts index e69de29bb..d07eecf0b 100644 --- a/apps/indexer/src/api/controllers/voting-power/listing.ts +++ b/apps/indexer/src/api/controllers/voting-power/listing.ts @@ -0,0 +1,90 @@ +import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi"; +import { VotingPowerService } from "@/api/services"; +import { + VotingPowersRequestSchema, + VotingPowersResponseSchema, + VotingPowersMapper, + VotingPowerResponseSchema, +} from "@/api/mappers/"; +import { isAddress } from "viem"; +import { VotingPowerMapper } from "@/api/mappers/voting-power/voting-power-variations"; + +export function votingPowers(app: Hono, service: VotingPowerService) { + app.openapi( + createRoute({ + method: "get", + operationId: "votingPowers", + path: "/voting-powers", + summary: "Get voting powers", + description: "TODO", + tags: ["proposals"], + request: { + query: VotingPowersRequestSchema, + }, + responses: { + 200: { + description: "Successfully retrieved voting power changes", + content: { + "application/json": { + schema: VotingPowersResponseSchema, + }, + }, + }, + }, + }), + async (context) => { + const { + limit, + skip, + orderDirection, + addresses, + powerGreaterThan, + powerLessThan, + } = context.req.valid("query"); + + const { items, totalCount } = await service.getVotingPowers( + skip, + limit, + orderDirection, + { + minAmount: powerGreaterThan, + maxAmount: powerLessThan, + }, + addresses, + ); + + return context.json(VotingPowersMapper(items, totalCount)); + }, + ); + + app.openapi( + createRoute({ + method: "get", + operationId: "votingPowerByAccountId", + path: "/voting-powers/{accountId}", + summary: "TODO", + description: "TODO", + tags: ["proposals"], + request: { + params: z.object({ + accountId: z.string().refine((addr) => isAddress(addr)), + }), + }, + responses: { + 200: { + description: "Successfully retrieved voting power changes", + content: { + "application/json": { + schema: VotingPowerResponseSchema, + }, + }, + }, + }, + }), + async (context) => { + const { accountId } = context.req.valid("param"); + const result = await service.getVotingPowersByAccountId(accountId); + return context.json(VotingPowerMapper(result)); + }, + ); +} diff --git a/apps/indexer/src/api/controllers/voting-power/variations.ts b/apps/indexer/src/api/controllers/voting-power/variations.ts index 5a023f63a..6060cbf51 100644 --- a/apps/indexer/src/api/controllers/voting-power/variations.ts +++ b/apps/indexer/src/api/controllers/voting-power/variations.ts @@ -1,4 +1,4 @@ -import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; +import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi"; import { VotingPowerService } from "@/api/services"; import { VotingPowerVariationsByAccountIdRequestSchema, @@ -8,7 +8,7 @@ import { VotingPowerVariationsResponseSchema, VotingPowerVariationsMapper, } from "@/api/mappers/"; -import { Address } from "viem"; +import { Address, isAddress } from "viem"; export function votingPowerVariations(app: Hono, service: VotingPowerService) { app.openapi( @@ -53,13 +53,16 @@ export function votingPowerVariations(app: Hono, service: VotingPowerService) { createRoute({ method: "get", operationId: "votingPowerVariationsByAccountId", - path: "/voting-powers/:accountId/variations", + path: "/voting-powers/{accountId}/variations", summary: "Get top changes in voting power for a given period for a single account", description: "Returns a the changes to voting power by period and accountId", tags: ["proposals"], request: { + params: z.object({ + accountId: z.string().refine((addr) => isAddress(addr)), + }), query: VotingPowerVariationsByAccountIdRequestSchema, }, responses: { @@ -74,7 +77,7 @@ export function votingPowerVariations(app: Hono, service: VotingPowerService) { }, }), async (context) => { - const accountId = context.req.param("accountId"); + const { accountId } = context.req.valid("param"); const { days } = context.req.valid("query"); const now = Math.floor(Date.now() / 1000); diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index 15b73b3be..1a74e9761 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -29,7 +29,6 @@ import { env } from "@/env"; import { DaoCache } from "@/api/cache/dao-cache"; import { DelegationPercentageRepository, - AccountBalanceRepository, DrizzleRepository, NFTPriceRepository, TokenRepository, @@ -38,13 +37,13 @@ import { DrizzleProposalsActivityRepository, NounsVotingPowerRepository, AccountInteractionsRepository, + AccountBalanceRepository, } from "@/api/repositories"; import { errorHandler } from "@/api/middlewares"; import { getClient } from "@/lib/client"; import { getChain } from "@/lib/utils"; import { DelegationPercentageService, - HistoricalVotingPowerService, VotingPowerService, TransactionsService, ProposalsService, @@ -53,8 +52,8 @@ import { NFTPriceService, TokenService, BalanceVariationsService, - HistoricalBalancesService, DaoService, + HistoricalBalancesService, } from "@/api/services"; import { CONTRACT_ADDRESSES } from "@/lib/constants"; import { DaoIdEnum } from "@/lib/enums"; @@ -101,6 +100,7 @@ if (!daoClient) { const { blockTime, tokenType } = CONTRACT_ADDRESSES[env.DAO_ID]; const repo = new DrizzleRepository(); +const accountBalanceRepo = new AccountBalanceRepository(); const votingPowerRepo = new VotingPowerRepository(); const proposalsRepo = new DrizzleProposalsActivityRepository(); const transactionsRepo = new TransactionsRepository(); @@ -108,7 +108,6 @@ const delegationPercentageRepo = new DelegationPercentageRepository(); const delegationPercentageService = new DelegationPercentageService( delegationPercentageRepo, ); -const accountBalanceRepo = new AccountBalanceRepository(); const accountInteractionRepo = new AccountInteractionsRepository(); const transactionsService = new TransactionsService(transactionsRepo); const votingPowerService = new VotingPowerService( @@ -157,7 +156,6 @@ proposals(app, new ProposalsService(repo, daoClient), daoClient, blockTime); historicalBalances( app, env.DAO_ID, - new HistoricalVotingPowerService(votingPowerRepo), new HistoricalBalancesService(accountBalanceRepo), ); transactions(app, transactionsService); diff --git a/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts b/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts index 07322f3b9..d336c6bbb 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts @@ -2,6 +2,7 @@ import { DaysEnum, DaysOpts } from "@/lib/enums"; import { z } from "@hono/zod-openapi"; import { PERCENTAGE_NO_BASELINE } from "../constants"; import { PeriodResponseMapper, PeriodResponseSchema } from "../shared"; +import { Address, isAddress } from "viem"; export const VotingPowerVariationsRequestSchema = z.object({ days: z @@ -33,6 +34,35 @@ export const VotingPowerVariationsByAccountIdRequestSchema = z.object({ .transform((val) => DaysEnum[val]), }); +export const VotingPowersRequestSchema = z.object({ + limit: z.coerce + .number() + .int() + .min(1, "Limit must be a positive integer") + .max(100, "Limit cannot exceed 100") + .optional() + .default(20), + skip: z.coerce + .number() + .int() + .min(0, "Skip must be a non-negative integer") + .optional() + .default(0), + orderDirection: z.enum(["asc", "desc"]).optional().default("desc"), + addresses: z + .array(z.string().refine((addr) => isAddress(addr))) + .optional() + .default([]), + powerGreaterThan: z + .string() + .transform((val) => BigInt(val)) + .optional(), + powerLessThan: z + .string() + .transform((val) => BigInt(val)) + .optional(), +}); + export const VotingPowerVariationResponseSchema = z.object({ accountId: z.string(), previousVotingPower: z.string().nullish(), @@ -41,6 +71,19 @@ export const VotingPowerVariationResponseSchema = z.object({ percentageChange: z.string(), }); +export const VotingPowerResponseSchema = z.object({ + accountId: z.string(), + votingPower: z.string(), + votesCount: z.number(), + proposalsCount: z.number(), + delegationsCount: z.number(), +}); + +export const VotingPowersResponseSchema = z.object({ + items: z.array(VotingPowerResponseSchema), + totalCount: z.number(), +}); + export const VotingPowerVariationsByAccountIdResponseSchema = z.object({ period: PeriodResponseSchema, data: VotingPowerVariationResponseSchema, @@ -63,6 +106,10 @@ export type VotingPowerVariationsByAccountIdResponse = z.infer< typeof VotingPowerVariationsByAccountIdResponseSchema >; +export type VotingPowersResponse = z.infer; + +export type VotingPowerResponse = z.infer; + export type DBVotingPowerVariation = { accountId: `0x${string}`; previousVotingPower: bigint | null; @@ -71,6 +118,14 @@ export type DBVotingPowerVariation = { percentageChange: number; }; +export type DBAccountPower = { + accountId: Address; + votingPower: bigint; + votesCount: number; + proposalsCount: number; + delegationsCount: number; +}; + export const VotingPowerVariationResponseMapper = ( delta: DBVotingPowerVariation, ): VotingPowerVariationResponse => ({ @@ -104,3 +159,25 @@ export const VotingPowerVariationsByAccountIdMapper = ( data: VotingPowerVariationResponseMapper(delta), }); }; + +export const VotingPowerMapper = ( + data: DBAccountPower, +): VotingPowerResponse => { + return VotingPowerResponseSchema.parse({ + accountId: data.accountId, + votingPower: data.votingPower.toString(), + votesCount: data.votesCount, + proposalsCount: data.proposalsCount, + delegationsCount: data.delegationsCount, + }); +}; + +export const VotingPowersMapper = ( + items: DBAccountPower[], + totalCount: number, +): VotingPowersResponse => { + return VotingPowersResponseSchema.parse({ + totalCount: totalCount, + items: items.map(VotingPowerMapper), + }); +}; diff --git a/apps/indexer/src/api/mappers/voting-power/voting-power.ts b/apps/indexer/src/api/mappers/voting-power/voting-power.ts index 7431d6727..4e71cbca1 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power.ts @@ -4,8 +4,8 @@ import { votingPowerHistory } from "ponder:schema"; import { DBDelegation, DBTransfer } from "../transactions"; import { isAddress } from "viem"; -export type DBVotingPower = typeof votingPowerHistory.$inferSelect; -export type DBVotingPowerWithRelations = DBVotingPower & { +export type DBHistoricalVotingPower = typeof votingPowerHistory.$inferSelect; +export type DBHistoricalVotingPowerWithRelations = DBHistoricalVotingPower & { delegations: DBDelegation | null; transfers: DBTransfer | null; }; @@ -31,7 +31,7 @@ export const HistoricalVotingPowerRequestSchema = z.object({ maxDelta: z.string().optional(), }); -export type VotingPowerRequest = z.infer< +export type HistoricalVotingPowerRequest = z.infer< typeof HistoricalVotingPowerRequestSchema >; @@ -64,14 +64,14 @@ export const HistoricalVotingPowerResponseSchema = z.object({ totalCount: z.number(), }); -export type VotingPowerResponse = z.infer< +export type HistoricalVotingPowerResponse = z.infer< typeof HistoricalVotingPowerResponseSchema >; -export const VotingPowerMapper = ( - p: DBVotingPowerWithRelations[], +export const HistoricalVotingPowerMapper = ( + p: DBHistoricalVotingPowerWithRelations[], totalCount: number, -): VotingPowerResponse => { +): HistoricalVotingPowerResponse => { return { items: p.map((p) => ({ transactionHash: p.transactionHash, diff --git a/apps/indexer/src/api/repositories/voting-power/general.ts b/apps/indexer/src/api/repositories/voting-power/general.ts index e8166d656..6e6fc273c 100644 --- a/apps/indexer/src/api/repositories/voting-power/general.ts +++ b/apps/indexer/src/api/repositories/voting-power/general.ts @@ -1,5 +1,5 @@ import { Address } from "viem"; -import { gte, and, lte, desc, eq, asc, sql } from "drizzle-orm"; +import { gte, and, lte, desc, eq, asc, sql, SQL, inArray } from "drizzle-orm"; import { db } from "ponder:api"; import { votingPowerHistory, @@ -9,12 +9,14 @@ import { } from "ponder:schema"; import { + AmountFilter, + DBAccountPower, DBVotingPowerVariation, - DBVotingPowerWithRelations, + DBHistoricalVotingPowerWithRelations, } from "@/api/mappers"; export class VotingPowerRepository { - async getVotingPowerCount( + async getHistoricalVotingPowerCount( accountId: Address, minDelta?: string, maxDelta?: string, @@ -41,7 +43,7 @@ export class VotingPowerRepository { orderBy: "timestamp" | "delta", minDelta?: string, maxDelta?: string, - ): Promise { + ): Promise { const result = await db .select() .from(votingPowerHistory) @@ -220,4 +222,83 @@ export class VotingPowerRepository { percentageChange: percentageChange, }; } + + async getVotingPowers( + skip: number, + limit: number, + orderDirection: "asc" | "desc", + amountFilter: AmountFilter, + addresses: Address[], + ): Promise<{ items: DBAccountPower[]; totalCount: number }> { + const baseQuery = db + .select() + .from(accountPower) + .where(this.filterToSql(addresses, amountFilter)); + + const [totalCount] = await db + .select({ + count: sql`COUNT(*)`.as("count"), + }) + .from(baseQuery.as("subquery")); + + const result = await baseQuery + .orderBy( + orderDirection === "desc" + ? desc(accountPower.votingPower) + : asc(accountPower.votingPower), + ) + .offset(skip) + .limit(limit); + + return { + items: result.map((r) => ({ + accountId: r.accountId, + votingPower: r.votingPower, + delegationsCount: r.delegationsCount, + votesCount: r.votesCount, + proposalsCount: r.proposalsCount, + })), + totalCount: Number(totalCount?.count ?? 0), + }; + } + + async getVotingPowersByAccountId( + accountId: Address, + ): Promise { + const [result] = await db + .select() + .from(accountPower) + .where(eq(accountPower.accountId, accountId)); + + if (!result) { + throw new Error("Account not found"); + } + + return { + accountId: result.accountId, + votingPower: result.votingPower, + delegationsCount: result.delegationsCount, + votesCount: result.votesCount, + proposalsCount: result.proposalsCount, + }; + } + + private filterToSql( + addresses: Address[], + amountfilter: AmountFilter, + ): SQL | undefined { + const conditions = []; + + if (addresses.length) { + conditions.push(inArray(accountPower.accountId, addresses)); + } + if (amountfilter.minAmount) { + gte(accountPower.votingPower, BigInt(amountfilter.minAmount)); + } + if (amountfilter.maxAmount) { + gte(accountPower.votingPower, BigInt(amountfilter.maxAmount)); + } + + return and(...conditions); + } } diff --git a/apps/indexer/src/api/repositories/voting-power/nouns.ts b/apps/indexer/src/api/repositories/voting-power/nouns.ts index 2a547dcd2..a03fe682d 100644 --- a/apps/indexer/src/api/repositories/voting-power/nouns.ts +++ b/apps/indexer/src/api/repositories/voting-power/nouns.ts @@ -3,10 +3,10 @@ import { gte, and, lte, desc, eq, asc, sql } from "drizzle-orm"; import { db } from "ponder:api"; import { votingPowerHistory, delegation, transfer } from "ponder:schema"; -import { DBVotingPowerWithRelations } from "@/api/mappers"; +import { DBHistoricalVotingPowerWithRelations } from "@/api/mappers"; export class NounsVotingPowerRepository { - async getVotingPowerCount( + async getHistoricalVotingPowerCount( accountId: Address, minDelta?: string, maxDelta?: string, @@ -33,7 +33,7 @@ export class NounsVotingPowerRepository { orderBy: "timestamp" | "delta", minDelta?: string, maxDelta?: string, - ): Promise { + ): Promise { const result = await db .select() .from(votingPowerHistory) diff --git a/apps/indexer/src/api/services/voting-power/index.ts b/apps/indexer/src/api/services/voting-power/index.ts index 2fd84e7e2..d791d1e95 100644 --- a/apps/indexer/src/api/services/voting-power/index.ts +++ b/apps/indexer/src/api/services/voting-power/index.ts @@ -1,11 +1,13 @@ import { Address } from "viem"; import { - DBVotingPowerWithRelations, + DBHistoricalVotingPowerWithRelations, DBVotingPowerVariation, + AmountFilter, + DBAccountPower, } from "@/api/mappers"; -interface VotingPowerRepository { +interface HistoricalVotingPowerRepository { getHistoricalVotingPowers( accountId: Address, skip: number, @@ -14,16 +16,16 @@ interface VotingPowerRepository { orderBy: "timestamp" | "delta", minDelta?: string, maxDelta?: string, - ): Promise; + ): Promise; - getVotingPowerCount( + getHistoricalVotingPowerCount( account: Address, minDelta?: string, maxDelta?: string, ): Promise; } -interface VotingPowerVariationRepository { +interface VotingPowersRepository { getVotingPowerVariations( startTimestamp: number, limit: number, @@ -35,12 +37,22 @@ interface VotingPowerVariationRepository { accountId: Address, startTimestamp: number, ): Promise; + + getVotingPowers( + skip: number, + limit: number, + orderDirection: "asc" | "desc", + amountFilter: AmountFilter, + addresses: Address[], + ): Promise<{ items: DBAccountPower[]; totalCount: number }>; + + getVotingPowersByAccountId(accountId: Address): Promise; } export class VotingPowerService { constructor( - private readonly votingRepository: VotingPowerRepository, - private readonly votingPowerVariationRepository: VotingPowerVariationRepository, + private readonly historicalVotingRepository: HistoricalVotingPowerRepository, + private readonly votingPowerRepository: VotingPowersRepository, ) {} async getHistoricalVotingPowers( @@ -51,22 +63,27 @@ export class VotingPowerService { orderBy: "timestamp" | "delta" = "timestamp", minDelta?: string, maxDelta?: string, - ): Promise<{ items: DBVotingPowerWithRelations[]; totalCount: number }> { - const items = await this.votingRepository.getHistoricalVotingPowers( - account, - skip, - limit, - orderDirection, - orderBy, - minDelta, - maxDelta, - ); + ): Promise<{ + items: DBHistoricalVotingPowerWithRelations[]; + totalCount: number; + }> { + const items = + await this.historicalVotingRepository.getHistoricalVotingPowers( + account, + skip, + limit, + orderDirection, + orderBy, + minDelta, + maxDelta, + ); - const totalCount = await this.votingRepository.getVotingPowerCount( - account, - minDelta, - maxDelta, - ); + const totalCount = + await this.historicalVotingRepository.getHistoricalVotingPowerCount( + account, + minDelta, + maxDelta, + ); return { items, totalCount }; } @@ -76,7 +93,7 @@ export class VotingPowerService { limit: number, orderDirection: "asc" | "desc", ): Promise { - return this.votingPowerVariationRepository.getVotingPowerVariations( + return this.votingPowerRepository.getVotingPowerVariations( startTimestamp, limit, skip, @@ -88,9 +105,31 @@ export class VotingPowerService { accountId: Address, startTimestamp: number, ): Promise { - return this.votingPowerVariationRepository.getVotingPowerVariationsByAccountId( + return this.votingPowerRepository.getVotingPowerVariationsByAccountId( accountId, startTimestamp, ); } + + async getVotingPowers( + skip: number, + limit: number, + orderDirection: "asc" | "desc", + amountFilter: AmountFilter, + addresses: Address[], + ): Promise<{ items: DBAccountPower[]; totalCount: number }> { + return this.votingPowerRepository.getVotingPowers( + limit, + skip, + orderDirection, + amountFilter, + addresses, + ); + } + + async getVotingPowersByAccountId( + accountId: Address, + ): Promise { + return this.votingPowerRepository.getVotingPowersByAccountId(accountId); + } } From 4c977dd55e4108bfce9290ec797cb56e3459fe7c Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Sun, 21 Dec 2025 21:56:34 -0300 Subject: [PATCH 4/8] chore(power-gql-to-rest): gql filter --- apps/api-gateway/src/resolvers/item.ts | 2 -- apps/api-gateway/src/resolvers/list.ts | 3 --- apps/api-gateway/src/resolvers/rest.ts | 22 ++++++++++--------- .../controllers/voting-power/historical.ts | 2 +- .../src/api/controllers/voting-power/index.ts | 1 + apps/indexer/src/api/index.ts | 6 +++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/api-gateway/src/resolvers/item.ts b/apps/api-gateway/src/resolvers/item.ts index bc08d62c2..43789743e 100644 --- a/apps/api-gateway/src/resolvers/item.ts +++ b/apps/api-gateway/src/resolvers/item.ts @@ -1,13 +1,11 @@ const daoItemQueries = [ "account", "accountBalance", - "accountPower", "daoMetricsDayBucket", "delegation", "proposalsOnchain", "transfer", "votesOnchain", - "votingPowerHistory", ]; export const itemResolvers = daoItemQueries.reduce((acc, fieldName) => { diff --git a/apps/api-gateway/src/resolvers/list.ts b/apps/api-gateway/src/resolvers/list.ts index 32772408e..8dde2e3bb 100644 --- a/apps/api-gateway/src/resolvers/list.ts +++ b/apps/api-gateway/src/resolvers/list.ts @@ -1,7 +1,5 @@ - const daoListQueries = [ 'accountBalances', - 'accountPowers', 'accounts', 'daoMetricsDayBuckets', 'delegations', @@ -9,7 +7,6 @@ const daoListQueries = [ 'tokens', 'transfers', 'votesOnchains', - 'votingPowerHistorys', ] export const listResolvers = daoListQueries.reduce((acc, fieldName) => { diff --git a/apps/api-gateway/src/resolvers/rest.ts b/apps/api-gateway/src/resolvers/rest.ts index 0e832aec1..42215fea8 100644 --- a/apps/api-gateway/src/resolvers/rest.ts +++ b/apps/api-gateway/src/resolvers/rest.ts @@ -1,5 +1,6 @@ const daoItemQueries = [ - 'accountInteractions', + "accountBalanceVariations", + "accountInteractions", "compareActiveSupply", "compareAverageTurnout", "compareCexSupply", @@ -11,23 +12,24 @@ const daoItemQueries = [ "compareTotalSupply", "compareTreasury", "compareVotes", + "dao", + "delegationPercentageByDay", "getTotalAssets", "getVotingPower", - "historicalTokenData", - "proposalsActivity", "historicalBalances", - "historicalVotingPower", - "proposals", - "transactions", + "historicalTokenData", + "historicalVotingPowers", "lastUpdate", "proposal", - "votingPowers", "proposalNonVoters", + "proposals", + "proposalsActivity", "token", + "transactions", + "votingPowerByAccountId", "votingPowerVariations", - "accountBalanceVariations", - "delegationPercentageByDay", - "dao", + "votingPowerVariationsByAccountId", + "votingPowers", ]; export const restResolvers = daoItemQueries.reduce((acc, fieldName) => { diff --git a/apps/indexer/src/api/controllers/voting-power/historical.ts b/apps/indexer/src/api/controllers/voting-power/historical.ts index d358d560e..2c2df3d48 100644 --- a/apps/indexer/src/api/controllers/voting-power/historical.ts +++ b/apps/indexer/src/api/controllers/voting-power/historical.ts @@ -7,7 +7,7 @@ import { HistoricalVotingPowerMapper, } from "@/api/mappers"; -export function votingPower(app: Hono, service: VotingPowerService) { +export function historicalVotingPowers(app: Hono, service: VotingPowerService) { app.openapi( createRoute({ method: "get", diff --git a/apps/indexer/src/api/controllers/voting-power/index.ts b/apps/indexer/src/api/controllers/voting-power/index.ts index cb880d8c5..2844f789f 100644 --- a/apps/indexer/src/api/controllers/voting-power/index.ts +++ b/apps/indexer/src/api/controllers/voting-power/index.ts @@ -1,2 +1,3 @@ export * from "./historical"; export * from "./variations"; +export * from "./listing"; diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index 1a74e9761..5bfee96ea 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -17,12 +17,13 @@ import { proposals, lastUpdate, totalAssets, - votingPower, + historicalVotingPowers, delegationPercentage, votingPowerVariations, accountBalanceVariations, dao, accountInteractions, + votingPowers, } from "@/api/controllers"; import { docs } from "@/api/docs"; import { env } from "@/env"; @@ -161,8 +162,9 @@ historicalBalances( transactions(app, transactionsService); lastUpdate(app); delegationPercentage(app, delegationPercentageService); -votingPower(app, votingPowerService); +historicalVotingPowers(app, votingPowerService); votingPowerVariations(app, votingPowerService); +votingPowers(app, votingPowerService); accountBalanceVariations(app, accountBalanceService); accountInteractions(app, accountBalanceService); dao(app, daoService); From 3b9b964a4d2c27e3ea1327b63e37f9a423732bb9 Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Mon, 22 Dec 2025 11:43:31 -0300 Subject: [PATCH 5/8] fix: small fix in votingPower endpoint --- apps/api-gateway/schema.graphql | 181 ++++++++++-------- .../api/repositories/voting-power/general.ts | 28 +-- .../src/api/services/voting-power/index.ts | 2 +- 3 files changed, 109 insertions(+), 102 deletions(-) diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 39bbe6f9e..162520b91 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -11,7 +11,7 @@ directive @httpOperation(subgraph: String, path: String, operationSpecificHeader directive @transport(subgraph: String, kind: String, location: String, headers: [[String]], queryStringOptions: ObjMap, queryParams: [[String]]) repeatable on SCHEMA type Query { - """Get property data for a specific token""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/token`\nGet property data for a specific token\n" token(currency: queryInput_token_currency = usd): token_200_response tokens(where: tokenFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): tokenPage! account(id: String!): account @@ -38,72 +38,58 @@ type Query { tokenPrices(where: tokenPriceFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): tokenPricePage! _meta: Meta - """Get total assets""" - totalAssets(days: queryInput_totalAssets_days = _7d): [query_totalAssets_items] - - """Get historical market data for a specific token""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/token/historical-data`\nGet historical market data for a specific token\n" historicalTokenData(skip: NonNegativeInt, limit: Float = 365): [query_historicalTokenData_items] - """Compare total supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/total-supply/compare`\nCompare total supply between periods\n" compareTotalSupply(days: queryInput_compareTotalSupply_days = _90d): compareTotalSupply_200_response - """Compare delegated supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/delegated-supply/compare`\nCompare delegated supply between periods\n" compareDelegatedSupply(days: queryInput_compareDelegatedSupply_days = _90d): compareDelegatedSupply_200_response - """Compare circulating supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/circulating-supply/compare`\nCompare circulating supply between periods\n" compareCirculatingSupply(days: queryInput_compareCirculatingSupply_days = _90d): compareCirculatingSupply_200_response - """Compare treasury between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/treasury/compare`\nCompare treasury between periods\n" compareTreasury(days: queryInput_compareTreasury_days = _90d): compareTreasury_200_response - """Compare cex supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/cex-supply/compare`\nCompare cex supply between periods\n" compareCexSupply(days: queryInput_compareCexSupply_days = _90d): compareCexSupply_200_response - """Compare dex supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/dex-supply/compare`\nCompare dex supply between periods\n" compareDexSupply(days: queryInput_compareDexSupply_days = _90d): compareDexSupply_200_response - """Compare lending supply between periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/lending-supply/compare`\nCompare lending supply between periods\n" compareLendingSupply(days: queryInput_compareLendingSupply_days = _90d): compareLendingSupply_200_response - """Get active token supply for DAO""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/active-supply/compare`\nGet active token supply for DAO\n" compareActiveSupply(days: queryInput_compareActiveSupply_days = _90d): compareActiveSupply_200_response - """Compare number of proposals between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals/compare`\nCompare number of proposals between time periods\n" compareProposals(days: queryInput_compareProposals_days = _90d): compareProposals_200_response - """Compare number of votes between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/votes/compare`\nCompare number of votes between time periods\n" compareVotes(days: queryInput_compareVotes_days = _90d): compareVotes_200_response - """Compare average turnout between time periods""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/average-turnout/compare`\nCompare average turnout between time periods\n" compareAverageTurnout(days: queryInput_compareAverageTurnout_days = _90d): compareAverageTurnout_200_response - """ - Returns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals-activity`\nReturns proposal activity data including voting history, win rates, and detailed proposal information for the specified delegate within the given time window\n" proposalsActivity(address: String!, fromDate: NonNegativeInt, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_proposalsActivity_orderBy = timestamp, orderDirection: queryInput_proposalsActivity_orderDirection = desc, userVoteFilter: queryInput_proposalsActivity_userVoteFilter): proposalsActivity_200_response - """Returns a list of proposal""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals`\nReturns a list of proposal\n" proposals(skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposals_orderDirection = desc, status: JSON, fromDate: Float, fromEndDate: Float, includeOptimisticProposals: queryInput_proposals_includeOptimisticProposals = TRUE): proposals_200_response - """Returns a single proposal by its ID""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals/{args.id}`\nReturns a single proposal by its ID\n" proposal(id: String!): proposal_200_response - """Returns the active delegates that did not vote on a given proposal""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/proposals/{args.id}/non-voters`\nReturns the active delegates that did not vote on a given proposal\n" proposalNonVoters(id: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderDirection: queryInput_proposalNonVoters_orderDirection = desc, addresses: JSON): proposalNonVoters_200_response - """ - Fetch historical token balances for multiple addresses at a specific time period using multicall - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/historical-balances`\nFetch historical token balances for multiple addresses at a specific time period using multicall\n" historicalBalances(addresses: JSON!, days: queryInput_historicalBalances_days = _7d): [query_historicalBalances_items] - """ - Fetch historical voting power for multiple addresses at a specific time period using multicall - """ - historicalVotingPower(addresses: JSON!, days: queryInput_historicalVotingPower_days = _7d, fromDate: Float): [query_historicalVotingPower_items] - - """ - Get transactions with their associated transfers and delegations, with optional filtering and sorting - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/transactions`\nGet transactions with their associated transfers and delegations, with optional filtering and sorting\n" transactions( limit: PositiveInt = 50 offset: NonNegativeInt @@ -125,32 +111,34 @@ type Query { includes: JSON ): transactions_200_response - """Get the last update time""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/last-update`\nGet the last update time\n" lastUpdate(chart: queryInput_lastUpdate_chart!): lastUpdate_200_response - """Get delegation percentage day buckets with forward-fill""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/delegation-percentage`\nGet delegation percentage day buckets with forward-fill\n" delegationPercentageByDay(startDate: String, endDate: String, orderDirection: queryInput_delegationPercentageByDay_orderDirection = asc, limit: NonNegativeInt = 365, after: String, before: String): delegationPercentageByDay_200_response - """Returns a list of voting power changes""" - votingPowers(account: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_votingPowers_orderBy = timestamp, orderDirection: queryInput_votingPowers_orderDirection = desc, minDelta: String, maxDelta: String): votingPowers_200_response + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/voting-powers/historical`\nReturns a list of voting power changes\n" + historicalVotingPowers(account: String!, skip: NonNegativeInt, limit: PositiveInt = 10, orderBy: queryInput_historicalVotingPowers_orderBy = timestamp, orderDirection: queryInput_historicalVotingPowers_orderDirection = desc, minDelta: String, maxDelta: String): historicalVotingPowers_200_response - """ - Returns a mapping of the biggest changes to voting power associated by delegate address - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/voting-powers/variations`\nReturns a mapping of the biggest changes to voting power associated by delegate address\n" votingPowerVariations(days: queryInput_votingPowerVariations_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_votingPowerVariations_orderDirection = desc): votingPowerVariations_200_response - """ - Returns a mapping of the biggest variations to account balances associated by account address - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/voting-powers/{args.accountId}/variations`\nReturns a the changes to voting power by period and accountId\n" + votingPowerVariationsByAccountId(accountId: String!, days: queryInput_votingPowerVariationsByAccountId_days = _90d): votingPowerVariationsByAccountId_200_response + + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/voting-powers`\nTODO\n" + votingPowers(limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_votingPowers_orderDirection = desc, addresses: [String] = [], powerGreaterThan: String, powerLessThan: String): votingPowers_200_response + + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/voting-powers/{args.accountId}`\nTODO\n" + votingPowerByAccountId(accountId: String!): votingPowerByAccountId_200_response + + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/account-balance/variations`\nReturns a mapping of the biggest variations to account balances associated by account address\n" accountBalanceVariations(days: queryInput_accountBalanceVariations_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountBalanceVariations_orderDirection = desc): accountBalanceVariations_200_response - """ - Returns a mapping of the largest interactions between accounts. - Positive amounts signify net token transfers FROM , whilst negative amounts refer to net transfers TO - """ + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/account-balance/interactions`\nReturns a mapping of the largest interactions between accounts. \nPositive amounts signify net token transfers FROM , whilst negative amounts refer to net transfers TO \n" accountInteractions(days: queryInput_accountInteractions_days = _90d, limit: PositiveInt = 20, skip: NonNegativeInt, orderDirection: queryInput_accountInteractions_orderDirection = desc, accountId: String!, minAmount: String, maxAmount: String, orderBy: queryInput_accountInteractions_orderBy = count, address: String): accountInteractions_200_response - """Returns current governance parameters for this DAO""" + "\n>**Method**: `GET`\n>**Base URL**: `http://localhost:42069`\n>**Path**: `/dao`\nReturns current governance parameters for this DAO\n" dao: dao_200_response """ @@ -1330,19 +1318,6 @@ input tokenPriceFilter { timestamp_lte: BigInt } -type query_totalAssets_items { - totalAssets: String! - date: String! -} - -enum queryInput_totalAssets_days { - _7d - _30d - _90d - _180d - _365d -} - type query_historicalTokenData_items { price: String! timestamp: Float! @@ -1679,19 +1654,6 @@ enum queryInput_historicalBalances_days { _365d } -type query_historicalVotingPower_items { - address: String! - votingPower: String! -} - -enum queryInput_historicalVotingPower_days { - _7d - _30d - _90d - _180d - _365d -} - type transactions_200_response { items: [query_transactions_items_items]! totalCount: Float! @@ -1781,12 +1743,12 @@ enum queryInput_delegationPercentageByDay_orderDirection { desc } -type votingPowers_200_response { - items: [query_votingPowers_items_items]! +type historicalVotingPowers_200_response { + items: [query_historicalVotingPowers_items_items]! totalCount: Float! } -type query_votingPowers_items_items { +type query_historicalVotingPowers_items_items { transactionHash: String! daoId: String! accountId: String! @@ -1794,28 +1756,28 @@ type query_votingPowers_items_items { delta: String! timestamp: String! logIndex: Float! - delegation: query_votingPowers_items_items_delegation - transfer: query_votingPowers_items_items_transfer + delegation: query_historicalVotingPowers_items_items_delegation + transfer: query_historicalVotingPowers_items_items_transfer } -type query_votingPowers_items_items_delegation { +type query_historicalVotingPowers_items_items_delegation { from: String! value: String! to: String! } -type query_votingPowers_items_items_transfer { +type query_historicalVotingPowers_items_items_transfer { value: String! from: String! to: String! } -enum queryInput_votingPowers_orderBy { +enum queryInput_historicalVotingPowers_orderBy { timestamp delta } -enum queryInput_votingPowers_orderDirection { +enum queryInput_historicalVotingPowers_orderDirection { asc desc } @@ -1852,6 +1814,59 @@ enum queryInput_votingPowerVariations_orderDirection { desc } +type votingPowerVariationsByAccountId_200_response { + period: query_votingPowerVariationsByAccountId_period! + data: query_votingPowerVariationsByAccountId_data! +} + +type query_votingPowerVariationsByAccountId_period { + days: String! + startTimestamp: String! + endTimestamp: String! +} + +type query_votingPowerVariationsByAccountId_data { + accountId: String! + previousVotingPower: String + currentVotingPower: String! + absoluteChange: String! + percentageChange: String! +} + +enum queryInput_votingPowerVariationsByAccountId_days { + _7d + _30d + _90d + _180d + _365d +} + +type votingPowers_200_response { + items: [query_votingPowers_items_items]! + totalCount: Float! +} + +type query_votingPowers_items_items { + accountId: String! + votingPower: String! + votesCount: Float! + proposalsCount: Float! + delegationsCount: Float! +} + +enum queryInput_votingPowers_orderDirection { + asc + desc +} + +type votingPowerByAccountId_200_response { + accountId: String! + votingPower: String! + votesCount: Float! + proposalsCount: Float! + delegationsCount: Float! +} + type accountBalanceVariations_200_response { period: query_accountBalanceVariations_period! items: [query_accountBalanceVariations_items_items]! diff --git a/apps/indexer/src/api/repositories/voting-power/general.ts b/apps/indexer/src/api/repositories/voting-power/general.ts index 6e6fc273c..ee1c7320e 100644 --- a/apps/indexer/src/api/repositories/voting-power/general.ts +++ b/apps/indexer/src/api/repositories/voting-power/general.ts @@ -203,13 +203,6 @@ export class VotingPowerRepository { const numericAbsoluteChange = BigInt(delta!.absoluteChange); const currentVotingPower = currentAccountPower.currentVotingPower; const oldVotingPower = currentVotingPower - numericAbsoluteChange; - console.log({ - numericAbsoluteChange: numericAbsoluteChange, - currentVotingPower: currentVotingPower, - oldVotingPower: oldVotingPower, - percentageChange: - Number((numericAbsoluteChange * 10000n) / oldVotingPower) / 100, - }); const percentageChange = oldVotingPower ? Number((numericAbsoluteChange * 10000n) / oldVotingPower) / 100 : 0; @@ -230,18 +223,10 @@ export class VotingPowerRepository { amountFilter: AmountFilter, addresses: Address[], ): Promise<{ items: DBAccountPower[]; totalCount: number }> { - const baseQuery = db + const result = await db .select() .from(accountPower) - .where(this.filterToSql(addresses, amountFilter)); - - const [totalCount] = await db - .select({ - count: sql`COUNT(*)`.as("count"), - }) - .from(baseQuery.as("subquery")); - - const result = await baseQuery + .where(this.filterToSql(addresses, amountFilter)) .orderBy( orderDirection === "desc" ? desc(accountPower.votingPower) @@ -250,6 +235,13 @@ export class VotingPowerRepository { .offset(skip) .limit(limit); + const [totalCount] = await db + .select({ + count: sql`COUNT(*)`.as("count"), + }) + .from(accountPower) + .where(this.filterToSql(addresses, amountFilter)); + return { items: result.map((r) => ({ accountId: r.accountId, @@ -299,6 +291,6 @@ export class VotingPowerRepository { gte(accountPower.votingPower, BigInt(amountfilter.maxAmount)); } - return and(...conditions); + return conditions.length ? and(...conditions) : sql`true`; } } diff --git a/apps/indexer/src/api/services/voting-power/index.ts b/apps/indexer/src/api/services/voting-power/index.ts index d791d1e95..4d06aa7f8 100644 --- a/apps/indexer/src/api/services/voting-power/index.ts +++ b/apps/indexer/src/api/services/voting-power/index.ts @@ -119,8 +119,8 @@ export class VotingPowerService { addresses: Address[], ): Promise<{ items: DBAccountPower[]; totalCount: number }> { return this.votingPowerRepository.getVotingPowers( - limit, skip, + limit, orderDirection, amountFilter, addresses, From 794b7906b5e74d2e20abb152169410db4bb7896b Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Tue, 6 Jan 2026 18:07:27 -0300 Subject: [PATCH 6/8] chore(power-gql-to-rest): filter generated queries --- apps/api-gateway/meshrc.ts | 12 +++++++++++- apps/api-gateway/schema.graphql | 4 ---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/api-gateway/meshrc.ts b/apps/api-gateway/meshrc.ts index 8dd2aff78..261b9e3b8 100644 --- a/apps/api-gateway/meshrc.ts +++ b/apps/api-gateway/meshrc.ts @@ -27,6 +27,16 @@ export default processConfig( }, }, transforms: [ + { + filterSchema: { + filters: [ + 'Query.!{accountPower}', + 'Query.!{accountPowers}', + 'Query.!{votingPowerHistory}', + 'Query.!{votingPowerHistorys}' + ] + } + }, { rename: { renames: [ @@ -57,7 +67,7 @@ export default processConfig( ]; }), ], - additionalTypeDefs:` + additionalTypeDefs: ` type AverageDelegationPercentageItem { date: String! high: String! diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 162520b91..868a955a2 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -18,10 +18,6 @@ type Query { accounts(where: accountFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): accountPage! accountBalance(accountId: String!, tokenId: String!): accountBalance accountBalances(where: accountBalanceFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): accountBalancePage! - accountPower(accountId: String!): accountPower - accountPowers(where: accountPowerFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): accountPowerPage! - votingPowerHistory(transactionHash: String!, accountId: String!, logIndex: Float!): votingPowerHistory - votingPowerHistorys(where: votingPowerHistoryFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): votingPowerHistoryPage! delegation(transactionHash: String!, delegatorAccountId: String!, delegateAccountId: String!): delegation delegations(where: delegationFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): delegationPage! transfer(transactionHash: String!, fromAccountId: String!, toAccountId: String!): transfer From 4c87add2a44ff8fb1c7a5f20ecbda1e175746829 Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Tue, 6 Jan 2026 18:07:27 -0300 Subject: [PATCH 7/8] chore(power-gql-to-rest): filter generated queries --- apps/api-gateway/meshrc.ts | 12 +++++++++- apps/api-gateway/schema.graphql | 4 ---- .../controllers/voting-power/historical.ts | 22 +++++++++---------- .../api/controllers/voting-power/listing.ts | 21 +++++++----------- .../voting-power/voting-power-variations.ts | 4 ++-- .../api/mappers/voting-power/voting-power.ts | 2 -- 6 files changed, 31 insertions(+), 34 deletions(-) diff --git a/apps/api-gateway/meshrc.ts b/apps/api-gateway/meshrc.ts index 8dd2aff78..261b9e3b8 100644 --- a/apps/api-gateway/meshrc.ts +++ b/apps/api-gateway/meshrc.ts @@ -27,6 +27,16 @@ export default processConfig( }, }, transforms: [ + { + filterSchema: { + filters: [ + 'Query.!{accountPower}', + 'Query.!{accountPowers}', + 'Query.!{votingPowerHistory}', + 'Query.!{votingPowerHistorys}' + ] + } + }, { rename: { renames: [ @@ -57,7 +67,7 @@ export default processConfig( ]; }), ], - additionalTypeDefs:` + additionalTypeDefs: ` type AverageDelegationPercentageItem { date: String! high: String! diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 162520b91..868a955a2 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -18,10 +18,6 @@ type Query { accounts(where: accountFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): accountPage! accountBalance(accountId: String!, tokenId: String!): accountBalance accountBalances(where: accountBalanceFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): accountBalancePage! - accountPower(accountId: String!): accountPower - accountPowers(where: accountPowerFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): accountPowerPage! - votingPowerHistory(transactionHash: String!, accountId: String!, logIndex: Float!): votingPowerHistory - votingPowerHistorys(where: votingPowerHistoryFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): votingPowerHistoryPage! delegation(transactionHash: String!, delegatorAccountId: String!, delegateAccountId: String!): delegation delegations(where: delegationFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): delegationPage! transfer(transactionHash: String!, fromAccountId: String!, toAccountId: String!): transfer diff --git a/apps/indexer/src/api/controllers/voting-power/historical.ts b/apps/indexer/src/api/controllers/voting-power/historical.ts index 2c2df3d48..9bc84fa64 100644 --- a/apps/indexer/src/api/controllers/voting-power/historical.ts +++ b/apps/indexer/src/api/controllers/voting-power/historical.ts @@ -1,4 +1,4 @@ -import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; +import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi"; import { VotingPowerService } from "@/api/services"; import { @@ -6,17 +6,21 @@ import { HistoricalVotingPowerRequestSchema, HistoricalVotingPowerMapper, } from "@/api/mappers"; +import { isAddress } from "viem"; export function historicalVotingPowers(app: Hono, service: VotingPowerService) { app.openapi( createRoute({ method: "get", operationId: "historicalVotingPowers", - path: "/voting-powers/historical", + path: "/voting-powers/{accountId}/historical", summary: "Get voting power changes", description: "Returns a list of voting power changes", tags: ["proposals"], request: { + params: z.object({ + accountId: z.string().refine((addr) => isAddress(addr)), + }), query: HistoricalVotingPowerRequestSchema, }, responses: { @@ -31,18 +35,12 @@ export function historicalVotingPowers(app: Hono, service: VotingPowerService) { }, }), async (context) => { - const { - account, - skip, - limit, - orderDirection, - orderBy, - minDelta, - maxDelta, - } = context.req.valid("query"); + const { skip, limit, orderDirection, orderBy, minDelta, maxDelta } = + context.req.valid("query"); + const { accountId } = context.req.valid("param"); const { items, totalCount } = await service.getHistoricalVotingPowers( - account, + accountId, skip, limit, orderDirection, diff --git a/apps/indexer/src/api/controllers/voting-power/listing.ts b/apps/indexer/src/api/controllers/voting-power/listing.ts index d07eecf0b..b778b797d 100644 --- a/apps/indexer/src/api/controllers/voting-power/listing.ts +++ b/apps/indexer/src/api/controllers/voting-power/listing.ts @@ -16,7 +16,7 @@ export function votingPowers(app: Hono, service: VotingPowerService) { operationId: "votingPowers", path: "/voting-powers", summary: "Get voting powers", - description: "TODO", + description: "Returns sorted and paginated account voting power records", tags: ["proposals"], request: { query: VotingPowersRequestSchema, @@ -33,22 +33,16 @@ export function votingPowers(app: Hono, service: VotingPowerService) { }, }), async (context) => { - const { - limit, - skip, - orderDirection, - addresses, - powerGreaterThan, - powerLessThan, - } = context.req.valid("query"); + const { limit, skip, orderDirection, addresses, fromValue, toValue } = + context.req.valid("query"); const { items, totalCount } = await service.getVotingPowers( skip, limit, orderDirection, { - minAmount: powerGreaterThan, - maxAmount: powerLessThan, + minAmount: fromValue, + maxAmount: toValue, }, addresses, ); @@ -62,8 +56,9 @@ export function votingPowers(app: Hono, service: VotingPowerService) { method: "get", operationId: "votingPowerByAccountId", path: "/voting-powers/{accountId}", - summary: "TODO", - description: "TODO", + summary: "Get account powers", + description: + "Returns voting power information for a specific address (account)", tags: ["proposals"], request: { params: z.object({ diff --git a/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts b/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts index d336c6bbb..752dd17d7 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts @@ -53,11 +53,11 @@ export const VotingPowersRequestSchema = z.object({ .array(z.string().refine((addr) => isAddress(addr))) .optional() .default([]), - powerGreaterThan: z + fromValue: z .string() .transform((val) => BigInt(val)) .optional(), - powerLessThan: z + toValue: z .string() .transform((val) => BigInt(val)) .optional(), diff --git a/apps/indexer/src/api/mappers/voting-power/voting-power.ts b/apps/indexer/src/api/mappers/voting-power/voting-power.ts index 4e71cbca1..ec76b24f1 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power.ts @@ -2,7 +2,6 @@ import { z } from "@hono/zod-openapi"; import { votingPowerHistory } from "ponder:schema"; import { DBDelegation, DBTransfer } from "../transactions"; -import { isAddress } from "viem"; export type DBHistoricalVotingPower = typeof votingPowerHistory.$inferSelect; export type DBHistoricalVotingPowerWithRelations = DBHistoricalVotingPower & { @@ -11,7 +10,6 @@ export type DBHistoricalVotingPowerWithRelations = DBHistoricalVotingPower & { }; export const HistoricalVotingPowerRequestSchema = z.object({ - account: z.string().refine((addr) => isAddress(addr)), skip: z.coerce .number() .int() From 4763e33310864579943d119682bc797439eeb936 Mon Sep 17 00:00:00 2001 From: Pedro Binotto Date: Thu, 8 Jan 2026 22:00:38 -0300 Subject: [PATCH 8/8] chore(port-power-query-to-rest): necessary server changes --- .../controllers/voting-power/historical.ts | 18 +++++++--- .../controllers/voting-power/variations.ts | 4 ++- .../voting-power/voting-power-variations.ts | 4 +++ .../api/mappers/voting-power/voting-power.ts | 8 +++-- .../api/repositories/voting-power/general.ts | 36 ++++++++++++++++--- .../api/repositories/voting-power/nouns.ts | 8 +++++ .../src/api/services/voting-power/index.ts | 9 +++++ 7 files changed, 76 insertions(+), 11 deletions(-) diff --git a/apps/indexer/src/api/controllers/voting-power/historical.ts b/apps/indexer/src/api/controllers/voting-power/historical.ts index 9bc84fa64..d0f66f6d8 100644 --- a/apps/indexer/src/api/controllers/voting-power/historical.ts +++ b/apps/indexer/src/api/controllers/voting-power/historical.ts @@ -35,8 +35,16 @@ export function historicalVotingPowers(app: Hono, service: VotingPowerService) { }, }), async (context) => { - const { skip, limit, orderDirection, orderBy, minDelta, maxDelta } = - context.req.valid("query"); + const { + skip, + limit, + orderDirection, + orderBy, + fromValue, + toValue, + fromDate, + toDate, + } = context.req.valid("query"); const { accountId } = context.req.valid("param"); const { items, totalCount } = await service.getHistoricalVotingPowers( @@ -45,8 +53,10 @@ export function historicalVotingPowers(app: Hono, service: VotingPowerService) { limit, orderDirection, orderBy, - minDelta, - maxDelta, + fromValue, + toValue, + fromDate, + toDate, ); return context.json(HistoricalVotingPowerMapper(items, totalCount)); }, diff --git a/apps/indexer/src/api/controllers/voting-power/variations.ts b/apps/indexer/src/api/controllers/voting-power/variations.ts index 6060cbf51..f7501f36b 100644 --- a/apps/indexer/src/api/controllers/voting-power/variations.ts +++ b/apps/indexer/src/api/controllers/voting-power/variations.ts @@ -35,10 +35,12 @@ export function votingPowerVariations(app: Hono, service: VotingPowerService) { }, }), async (context) => { - const { days, limit, skip, orderDirection } = context.req.valid("query"); + const { addresses, days, limit, skip, orderDirection } = + context.req.valid("query"); const now = Math.floor(Date.now() / 1000); const result = await service.getVotingPowerVariations( + addresses, now - days, skip, limit, diff --git a/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts b/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts index 752dd17d7..35ae74fed 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power-variations.ts @@ -5,6 +5,10 @@ import { PeriodResponseMapper, PeriodResponseSchema } from "../shared"; import { Address, isAddress } from "viem"; export const VotingPowerVariationsRequestSchema = z.object({ + addresses: z + .array(z.string().refine((addr) => isAddress(addr))) + .optional() + .default([]), days: z .enum(DaysOpts) .optional() diff --git a/apps/indexer/src/api/mappers/voting-power/voting-power.ts b/apps/indexer/src/api/mappers/voting-power/voting-power.ts index ec76b24f1..cec0e1f44 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power.ts @@ -25,8 +25,10 @@ export const HistoricalVotingPowerRequestSchema = z.object({ .default(10), orderBy: z.enum(["timestamp", "delta"]).optional().default("timestamp"), orderDirection: z.enum(["asc", "desc"]).optional().default("desc"), - minDelta: z.string().optional(), - maxDelta: z.string().optional(), + fromDate: z.coerce.number().optional(), + toDate: z.coerce.number().optional(), + fromValue: z.string().optional(), + toValue: z.string().optional(), }); export type HistoricalVotingPowerRequest = z.infer< @@ -48,6 +50,7 @@ export const HistoricalVotingPowerResponseSchema = z.object({ from: z.string(), value: z.string(), to: z.string(), + previousDelegate: z.string().nullable(), }) .nullable(), transfer: z @@ -84,6 +87,7 @@ export const HistoricalVotingPowerMapper = ( from: p.delegations.delegatorAccountId, value: p.delegations.delegatedValue.toString(), to: p.delegations.delegateAccountId, + previousDelegate: p.delegations.previousDelegate, } : null, transfer: p.transfers diff --git a/apps/indexer/src/api/repositories/voting-power/general.ts b/apps/indexer/src/api/repositories/voting-power/general.ts index ee1c7320e..b6d78e8ef 100644 --- a/apps/indexer/src/api/repositories/voting-power/general.ts +++ b/apps/indexer/src/api/repositories/voting-power/general.ts @@ -1,5 +1,17 @@ import { Address } from "viem"; -import { gte, and, lte, desc, eq, asc, sql, SQL, inArray } from "drizzle-orm"; +import { + and, + desc, + eq, + asc, + sql, + SQL, + inArray, + gt, + lt, + gte, + lte, +} from "drizzle-orm"; import { db } from "ponder:api"; import { votingPowerHistory, @@ -43,6 +55,8 @@ export class VotingPowerRepository { orderBy: "timestamp" | "delta", minDelta?: string, maxDelta?: string, + fromDate?: number, + toDate?: number, ): Promise { const result = await db .select() @@ -56,6 +70,12 @@ export class VotingPowerRepository { maxDelta ? lte(votingPowerHistory.deltaMod, BigInt(maxDelta)) : undefined, + fromDate + ? gte(votingPowerHistory.timestamp, BigInt(fromDate)) + : undefined, + toDate + ? lte(votingPowerHistory.timestamp, BigInt(toDate)) + : undefined, ), ) .leftJoin( @@ -110,6 +130,7 @@ export class VotingPowerRepository { } async getVotingPowerVariations( + addresses: Address[], startTimestamp: number, limit: number, skip: number, @@ -122,7 +143,14 @@ export class VotingPowerRepository { }) .from(votingPowerHistory) .orderBy(desc(votingPowerHistory.timestamp)) - .where(gte(votingPowerHistory.timestamp, BigInt(startTimestamp))) + .where( + and( + gte(votingPowerHistory.timestamp, BigInt(startTimestamp)), + addresses.length + ? inArray(votingPowerHistory.accountId, addresses) + : undefined, + ), + ) .as("history"); const aggregate = db @@ -285,10 +313,10 @@ export class VotingPowerRepository { conditions.push(inArray(accountPower.accountId, addresses)); } if (amountfilter.minAmount) { - gte(accountPower.votingPower, BigInt(amountfilter.minAmount)); + gt(accountPower.votingPower, BigInt(amountfilter.minAmount)); } if (amountfilter.maxAmount) { - gte(accountPower.votingPower, BigInt(amountfilter.maxAmount)); + lt(accountPower.votingPower, BigInt(amountfilter.maxAmount)); } return conditions.length ? and(...conditions) : sql`true`; diff --git a/apps/indexer/src/api/repositories/voting-power/nouns.ts b/apps/indexer/src/api/repositories/voting-power/nouns.ts index a03fe682d..b4aa1dd9a 100644 --- a/apps/indexer/src/api/repositories/voting-power/nouns.ts +++ b/apps/indexer/src/api/repositories/voting-power/nouns.ts @@ -33,6 +33,8 @@ export class NounsVotingPowerRepository { orderBy: "timestamp" | "delta", minDelta?: string, maxDelta?: string, + fromDate?: number, + toDate?: number, ): Promise { const result = await db .select() @@ -46,6 +48,12 @@ export class NounsVotingPowerRepository { maxDelta ? lte(votingPowerHistory.deltaMod, BigInt(maxDelta)) : undefined, + fromDate + ? gte(votingPowerHistory.timestamp, BigInt(fromDate)) + : undefined, + toDate + ? lte(votingPowerHistory.timestamp, BigInt(toDate)) + : undefined, ), ) .leftJoin( diff --git a/apps/indexer/src/api/services/voting-power/index.ts b/apps/indexer/src/api/services/voting-power/index.ts index 4d06aa7f8..693ac5c52 100644 --- a/apps/indexer/src/api/services/voting-power/index.ts +++ b/apps/indexer/src/api/services/voting-power/index.ts @@ -16,6 +16,8 @@ interface HistoricalVotingPowerRepository { orderBy: "timestamp" | "delta", minDelta?: string, maxDelta?: string, + fromDate?: number, + toDate?: number, ): Promise; getHistoricalVotingPowerCount( @@ -27,6 +29,7 @@ interface HistoricalVotingPowerRepository { interface VotingPowersRepository { getVotingPowerVariations( + addresses: Address[], startTimestamp: number, limit: number, skip: number, @@ -63,6 +66,8 @@ export class VotingPowerService { orderBy: "timestamp" | "delta" = "timestamp", minDelta?: string, maxDelta?: string, + fromDate?: number, + toDate?: number, ): Promise<{ items: DBHistoricalVotingPowerWithRelations[]; totalCount: number; @@ -76,6 +81,8 @@ export class VotingPowerService { orderBy, minDelta, maxDelta, + fromDate, + toDate, ); const totalCount = @@ -88,12 +95,14 @@ export class VotingPowerService { } async getVotingPowerVariations( + addresses: Address[], startTimestamp: number, skip: number, limit: number, orderDirection: "asc" | "desc", ): Promise { return this.votingPowerRepository.getVotingPowerVariations( + addresses, startTimestamp, limit, skip,