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 39bbe6f9e..868a955a2 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -11,17 +11,13 @@ 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 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 @@ -38,72 +34,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 +107,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 +1314,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 +1650,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 +1739,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 +1752,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 +1810,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/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/account-balance/historical-balances.ts b/apps/indexer/src/api/controllers/account-balance/historical-balances.ts index f33a9500f..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 @@ -82,64 +78,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/historical.ts b/apps/indexer/src/api/controllers/voting-power/historical.ts new file mode 100644 index 000000000..d0f66f6d8 --- /dev/null +++ b/apps/indexer/src/api/controllers/voting-power/historical.ts @@ -0,0 +1,64 @@ +import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi"; + +import { VotingPowerService } from "@/api/services"; +import { + HistoricalVotingPowerResponseSchema, + 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/{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: { + 200: { + description: "Successfully retrieved voting power changes", + content: { + "application/json": { + schema: HistoricalVotingPowerResponseSchema, + }, + }, + }, + }, + }), + async (context) => { + 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( + accountId, + skip, + limit, + orderDirection, + orderBy, + fromValue, + toValue, + fromDate, + toDate, + ); + return context.json(HistoricalVotingPowerMapper(items, totalCount)); + }, + ); +} diff --git a/apps/indexer/src/api/controllers/voting-power/index.ts b/apps/indexer/src/api/controllers/voting-power/index.ts index 0e12f3c12..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 "./voting-powers"; -export * from "./voting-power-variations"; +export * from "./historical"; +export * from "./variations"; +export * from "./listing"; 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..b778b797d --- /dev/null +++ b/apps/indexer/src/api/controllers/voting-power/listing.ts @@ -0,0 +1,85 @@ +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: "Returns sorted and paginated account voting power records", + 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, fromValue, toValue } = + context.req.valid("query"); + + const { items, totalCount } = await service.getVotingPowers( + skip, + limit, + orderDirection, + { + minAmount: fromValue, + maxAmount: toValue, + }, + addresses, + ); + + return context.json(VotingPowersMapper(items, totalCount)); + }, + ); + + app.openapi( + createRoute({ + method: "get", + operationId: "votingPowerByAccountId", + path: "/voting-powers/{accountId}", + summary: "Get account powers", + description: + "Returns voting power information for a specific address (account)", + 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 new file mode 100644 index 000000000..f7501f36b --- /dev/null +++ b/apps/indexer/src/api/controllers/voting-power/variations.ts @@ -0,0 +1,96 @@ +import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi"; +import { VotingPowerService } from "@/api/services"; +import { + VotingPowerVariationsByAccountIdRequestSchema, + VotingPowerVariationsByAccountIdResponseSchema, + VotingPowerVariationsByAccountIdMapper, + VotingPowerVariationsRequestSchema, + VotingPowerVariationsResponseSchema, + VotingPowerVariationsMapper, +} from "@/api/mappers/"; +import { Address, isAddress } from "viem"; + +export function votingPowerVariations(app: Hono, service: VotingPowerService) { + app.openapi( + createRoute({ + method: "get", + operationId: "votingPowerVariations", + 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", + tags: ["proposals"], + request: { + query: VotingPowerVariationsRequestSchema, + }, + responses: { + 200: { + description: "Successfully retrieved voting power changes", + content: { + "application/json": { + schema: VotingPowerVariationsResponseSchema, + }, + }, + }, + }, + }), + async (context) => { + 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, + orderDirection, + ); + + return context.json(VotingPowerVariationsMapper(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: { + params: z.object({ + accountId: z.string().refine((addr) => isAddress(addr)), + }), + query: VotingPowerVariationsByAccountIdRequestSchema, + }, + responses: { + 200: { + description: "Successfully retrieved voting power changes", + content: { + "application/json": { + schema: VotingPowerVariationsByAccountIdResponseSchema, + }, + }, + }, + }, + }), + async (context) => { + const { accountId } = context.req.valid("param"); + 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/controllers/voting-power/voting-power-variations.ts b/apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts deleted file mode 100644 index fc0e4e76a..000000000 --- a/apps/indexer/src/api/controllers/voting-power/voting-power-variations.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; -import { VotingPowerService } from "@/api/services"; -import { - VotingPowerVariationsMapper, - VotingPowerVariationsRequestSchema, - VotingPowerVariationsResponseSchema, -} from "@/api/mappers/"; - -export function votingPowerVariations(app: Hono, service: VotingPowerService) { - app.openapi( - createRoute({ - method: "get", - operationId: "votingPowerVariations", - path: "/voting-power/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", - tags: ["proposals"], - request: { - query: VotingPowerVariationsRequestSchema, - }, - responses: { - 200: { - description: "Successfully retrieved voting power changes", - content: { - "application/json": { - schema: VotingPowerVariationsResponseSchema, - }, - }, - }, - }, - }), - async (context) => { - const { days, limit, skip, orderDirection } = context.req.valid("query"); - const now = Math.floor(Date.now() / 1000); - - const result = await service.getVotingPowerVariations( - now - days, - skip, - limit, - orderDirection, - ); - - return context.json(VotingPowerVariationsMapper(result, now, days)); - }, - ); -} diff --git a/apps/indexer/src/api/controllers/voting-power/voting-powers.ts b/apps/indexer/src/api/controllers/voting-power/voting-powers.ts deleted file mode 100644 index dd7430722..000000000 --- a/apps/indexer/src/api/controllers/voting-power/voting-powers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { OpenAPIHono as Hono, createRoute } from "@hono/zod-openapi"; - -import { VotingPowerService } from "@/api/services"; -import { - VotingPowerResponseSchema, - VotingPowerRequestSchema, - VotingPowerMapper, -} from "@/api/mappers"; - -export function votingPower(app: Hono, service: VotingPowerService) { - app.openapi( - createRoute({ - method: "get", - operationId: "votingPowers", - path: "/voting-powers", - summary: "Get voting power changes", - description: "Returns a list of voting power changes", - tags: ["proposals"], - request: { - query: VotingPowerRequestSchema, - }, - responses: { - 200: { - description: "Successfully retrieved voting power changes", - content: { - "application/json": { - schema: VotingPowerResponseSchema, - }, - }, - }, - }, - }), - async (context) => { - const { - account, - skip, - limit, - orderDirection, - orderBy, - minDelta, - maxDelta, - } = context.req.valid("query"); - - const { items, totalCount } = await service.getVotingPowers( - account, - skip, - limit, - orderDirection, - orderBy, - minDelta, - maxDelta, - ); - return context.json(VotingPowerMapper(items, totalCount)); - }, - ); -} diff --git a/apps/indexer/src/api/index.ts b/apps/indexer/src/api/index.ts index f31d2c557..a143f391c 100644 --- a/apps/indexer/src/api/index.ts +++ b/apps/indexer/src/api/index.ts @@ -17,19 +17,19 @@ import { proposals, lastUpdate, totalAssets, - votingPower, + historicalVotingPowers, delegationPercentage, votingPowerVariations, accountBalanceVariations, dao, accountInteractions, + votingPowers, } from "@/api/controllers"; import { docs } from "@/api/docs"; import { env } from "@/env"; import { DaoCache } from "@/api/cache/dao-cache"; import { DelegationPercentageRepository, - AccountBalanceRepository, DrizzleRepository, NFTPriceRepository, TokenRepository, @@ -38,13 +38,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 +53,8 @@ import { NFTPriceService, TokenService, BalanceVariationsService, - HistoricalBalancesService, DaoService, + HistoricalBalancesService, } from "@/api/services"; import { CONTRACT_ADDRESSES } from "@/lib/constants"; import { DaoIdEnum } from "@/lib/enums"; @@ -106,6 +106,7 @@ const optimisticProposalType = : undefined; const repo = new DrizzleRepository(); +const accountBalanceRepo = new AccountBalanceRepository(); const votingPowerRepo = new VotingPowerRepository(); const proposalsRepo = new DrizzleProposalsActivityRepository(); const transactionsRepo = new TransactionsRepository(); @@ -113,7 +114,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( @@ -167,14 +167,14 @@ proposals( historicalBalances( app, env.DAO_ID, - new HistoricalVotingPowerService(votingPowerRepo), new HistoricalBalancesService(accountBalanceRepo), ); 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); diff --git a/apps/indexer/src/api/mappers/account-balance/index.ts b/apps/indexer/src/api/mappers/account-balance/index.ts index 7e3780b7b..89ce2c069 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, isAddress } 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(), @@ -65,11 +62,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({ @@ -148,11 +141,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, @@ -179,11 +168,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.map( ({ accountId, absoluteChange, totalVolume, transferCount }) => ({ diff --git a/apps/indexer/src/api/mappers/index.ts b/apps/indexer/src/api/mappers/index.ts index a40f25bdd..35f8b0def 100644 --- a/apps/indexer/src/api/mappers/index.ts +++ b/apps/indexer/src/api/mappers/index.ts @@ -6,3 +6,4 @@ export * from "./token"; export * from "./delegation-percentage"; export * from "./account-balance"; export * from "./dao"; +export * from "./shared"; diff --git a/apps/indexer/src/api/mappers/shared.ts b/apps/indexer/src/api/mappers/shared.ts new file mode 100644 index 000000000..bf0729c32 --- /dev/null +++ b/apps/indexer/src/api/mappers/shared.ts @@ -0,0 +1,24 @@ +import { DaysEnum } from "@/lib/enums"; +import { z } from "@hono/zod-openapi"; + +export type AmountFilter = { + minAmount: number | bigint | undefined; + maxAmount: number | bigint | undefined; +}; + +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..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 @@ -1,8 +1,14 @@ 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({ + addresses: z + .array(z.string().refine((addr) => isAddress(addr))) + .optional() + .default([]), days: z .enum(DaysOpts) .optional() @@ -24,27 +30,90 @@ 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 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([]), + fromValue: z + .string() + .transform((val) => BigInt(val)) + .optional(), + toValue: z + .string() + .transform((val) => BigInt(val)) + .optional(), +}); + +export const VotingPowerVariationResponseSchema = z.object({ + accountId: z.string(), + previousVotingPower: z.string().nullish(), + currentVotingPower: z.string(), + absoluteChange: z.string(), + 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, +}); + 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 VotingPowersResponse = z.infer; + +export type VotingPowerResponse = z.infer; + export type DBVotingPowerVariation = { accountId: `0x${string}`; previousVotingPower: bigint | null; @@ -53,33 +122,66 @@ export type DBVotingPowerVariation = { percentageChange: number; }; +export type DBAccountPower = { + accountId: Address; + votingPower: bigint; + votesCount: number; + proposalsCount: number; + delegationsCount: 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), + }); +}; + +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 a0b0b4946..cec0e1f44 100644 --- a/apps/indexer/src/api/mappers/voting-power/voting-power.ts +++ b/apps/indexer/src/api/mappers/voting-power/voting-power.ts @@ -2,16 +2,14 @@ import { z } from "@hono/zod-openapi"; 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; }; -export const VotingPowerRequestSchema = z.object({ - account: z.string().refine((addr) => isAddress(addr)), +export const HistoricalVotingPowerRequestSchema = z.object({ skip: z.coerce .number() .int() @@ -27,13 +25,17 @@ export const VotingPowerRequestSchema = 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 VotingPowerRequest = z.infer; +export type HistoricalVotingPowerRequest = z.infer< + typeof HistoricalVotingPowerRequestSchema +>; -export const VotingPowerResponseSchema = z.object({ +export const HistoricalVotingPowerResponseSchema = z.object({ items: z.array( z.object({ transactionHash: z.string(), @@ -48,6 +50,7 @@ export const VotingPowerResponseSchema = z.object({ from: z.string(), value: z.string(), to: z.string(), + previousDelegate: z.string().nullable(), }) .nullable(), transfer: z @@ -62,12 +65,14 @@ export const VotingPowerResponseSchema = 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, @@ -82,6 +87,7 @@ export const VotingPowerMapper = ( 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 afbfaf93a..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, inArray, lte, desc, eq, asc, sql } 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, @@ -9,34 +21,14 @@ import { } from "ponder:schema"; import { + AmountFilter, + DBAccountPower, DBVotingPowerVariation, - DBVotingPowerWithRelations, + DBHistoricalVotingPowerWithRelations, } 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( + async getHistoricalVotingPowerCount( accountId: Address, minDelta?: string, maxDelta?: string, @@ -55,7 +47,7 @@ export class VotingPowerRepository { ); } - async getVotingPowers( + async getHistoricalVotingPowers( accountId: Address, skip: number, limit: number, @@ -63,7 +55,9 @@ export class VotingPowerRepository { orderBy: "timestamp" | "delta", minDelta?: string, maxDelta?: string, - ): Promise { + fromDate?: number, + toDate?: number, + ): Promise { const result = await db .select() .from(votingPowerHistory) @@ -76,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( @@ -129,7 +129,8 @@ export class VotingPowerRepository { })); } - async getVotingPowerChanges( + async getVotingPowerVariations( + addresses: Address[], startTimestamp: number, limit: number, skip: number, @@ -142,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 @@ -183,4 +191,134 @@ export class VotingPowerRepository { }; }); } + + async getVotingPowerVariationsByAccountId( + accountId: Address, + startTimestamp: number, + ): Promise { + const history = db + .select({ + accountId: votingPowerHistory.accountId, + delta: votingPowerHistory.delta, + }) + .from(votingPowerHistory) + .orderBy(desc(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 }) + .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, + }; + } + + async getVotingPowers( + skip: number, + limit: number, + orderDirection: "asc" | "desc", + amountFilter: AmountFilter, + addresses: Address[], + ): Promise<{ items: DBAccountPower[]; totalCount: number }> { + const result = await db + .select() + .from(accountPower) + .where(this.filterToSql(addresses, amountFilter)) + .orderBy( + orderDirection === "desc" + ? desc(accountPower.votingPower) + : asc(accountPower.votingPower), + ) + .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, + 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) { + gt(accountPower.votingPower, BigInt(amountfilter.minAmount)); + } + if (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 9817d0eea..b4aa1dd9a 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, @@ -25,7 +25,7 @@ export class NounsVotingPowerRepository { ); } - async getVotingPowers( + async getHistoricalVotingPowers( accountId: Address, skip: number, limit: number, @@ -33,7 +33,9 @@ export class NounsVotingPowerRepository { orderBy: "timestamp" | "delta", minDelta?: string, maxDelta?: string, - ): Promise { + fromDate?: number, + toDate?: number, + ): Promise { const result = await db .select() .from(votingPowerHistory) @@ -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/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..693ac5c52 100644 --- a/apps/indexer/src/api/services/voting-power/index.ts +++ b/apps/indexer/src/api/services/voting-power/index.ts @@ -1,2 +1,144 @@ -export * from "./historical-voting-power"; -export * from "./voting-power"; +import { Address } from "viem"; + +import { + DBHistoricalVotingPowerWithRelations, + DBVotingPowerVariation, + AmountFilter, + DBAccountPower, +} from "@/api/mappers"; + +interface HistoricalVotingPowerRepository { + getHistoricalVotingPowers( + accountId: Address, + skip: number, + limit: number, + orderDirection: "asc" | "desc", + orderBy: "timestamp" | "delta", + minDelta?: string, + maxDelta?: string, + fromDate?: number, + toDate?: number, + ): Promise; + + getHistoricalVotingPowerCount( + account: Address, + minDelta?: string, + maxDelta?: string, + ): Promise; +} + +interface VotingPowersRepository { + getVotingPowerVariations( + addresses: Address[], + startTimestamp: number, + limit: number, + skip: number, + orderDirection: "asc" | "desc", + ): Promise; + + getVotingPowerVariationsByAccountId( + 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 historicalVotingRepository: HistoricalVotingPowerRepository, + private readonly votingPowerRepository: VotingPowersRepository, + ) {} + + async getHistoricalVotingPowers( + account: Address, + skip: number, + limit: number, + orderDirection: "asc" | "desc" = "desc", + orderBy: "timestamp" | "delta" = "timestamp", + minDelta?: string, + maxDelta?: string, + fromDate?: number, + toDate?: number, + ): Promise<{ + items: DBHistoricalVotingPowerWithRelations[]; + totalCount: number; + }> { + const items = + await this.historicalVotingRepository.getHistoricalVotingPowers( + account, + skip, + limit, + orderDirection, + orderBy, + minDelta, + maxDelta, + fromDate, + toDate, + ); + + const totalCount = + await this.historicalVotingRepository.getHistoricalVotingPowerCount( + account, + minDelta, + maxDelta, + ); + return { items, totalCount }; + } + + async getVotingPowerVariations( + addresses: Address[], + startTimestamp: number, + skip: number, + limit: number, + orderDirection: "asc" | "desc", + ): Promise { + return this.votingPowerRepository.getVotingPowerVariations( + addresses, + startTimestamp, + limit, + skip, + orderDirection, + ); + } + + async getVotingPowerVariationsByAccountId( + accountId: Address, + startTimestamp: number, + ): Promise { + 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( + skip, + limit, + orderDirection, + amountFilter, + addresses, + ); + } + + async getVotingPowersByAccountId( + accountId: Address, + ): Promise { + return this.votingPowerRepository.getVotingPowersByAccountId(accountId); + } +} 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 f2ae28ce1..000000000 --- a/apps/indexer/src/api/services/voting-power/voting-power.ts +++ /dev/null @@ -1,81 +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 { - getVotingPowerChanges( - startTimestamp: number, - limit: number, - skip: number, - orderDirection: "asc" | "desc", - ): 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.getVotingPowerChanges( - startTimestamp, - limit, - skip, - orderDirection, - ); - } -}