Skip to content

Commit

Permalink
Index x/gov proposals.
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Jan 6, 2024
1 parent 7f42482 commit 91db149
Show file tree
Hide file tree
Showing 21 changed files with 951 additions and 10 deletions.
4 changes: 2 additions & 2 deletions compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ services:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
test: ['CMD-SHELL', 'pg_isready -U test']
interval: 1s
timeout: 3s
retries: 5

redis:
image: redis:7-alpine
healthcheck:
test: ["CMD-SHELL", "redis-cli ping"]
test: ['CMD-SHELL', 'redis-cli ping']
interval: 1s
timeout: 3s
retries: 5
Expand Down
1 change: 1 addition & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This returns the status of the indexer.
"lastStakingBlockHeightExported": string | null
"lastWasmBlockHeightExported": string | null
"lastBankBlockHeightExported": string | null
"lastGovBlockHeightExported": string | null
}
```

Expand Down
122 changes: 122 additions & 0 deletions src/core/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Op, Sequelize } from 'sequelize'
import {
BankStateEvent,
Contract,
GovStateEvent,
StakingSlashEvent,
WasmStateEvent,
WasmStateEventTransformation,
Expand All @@ -26,6 +27,8 @@ import {
FormulaMapGetter,
FormulaPrefetch,
FormulaPrefetchTransformations,
FormulaProposalGetter,
FormulaProposalsGetter,
FormulaSlashEventsGetter,
FormulaTransformationDateGetter,
FormulaTransformationMapGetter,
Expand Down Expand Up @@ -1090,6 +1093,122 @@ export const getEnv = ({
)
}

const getProposal: FormulaProposalGetter = async (proposalId) => {
const dependentKey = getDependentKey(
GovStateEvent.dependentKeyNamespace,
proposalId
)
dependentKeys?.push({
key: dependentKey,
prefix: false,
})

// Check cache.
const cachedEvent = cache.events[dependentKey]
const event =
// If undefined, we haven't tried to fetch it yet. If not undefined,
// either it exists or it doesn't (null).
cachedEvent !== undefined
? cachedEvent?.[0]
: await GovStateEvent.findOne({
where: {
proposalId,
blockHeight: blockHeightFilter,
},
order: [['blockHeight', 'DESC']],
})

// Type-check. Should never happen assuming dependent key namespaces are
// unique across different event types.
if (event && !(event instanceof GovStateEvent)) {
throw new Error('Incorrect event type.')
}

// Cache event, null if nonexistent.
if (cachedEvent === undefined) {
cache.events[dependentKey] = event ? [event] : null
}

// If no event found, return undefined.
if (!event) {
return
}

// Call hook.
await onFetch?.([event])

return event.value
}

const getProposals: FormulaProposalsGetter = async () => {
const dependentKey =
getDependentKey(GovStateEvent.dependentKeyNamespace) + ':'
dependentKeys?.push({
key: dependentKey,
prefix: true,
})

// Check cache.
const cachedEvents = cache.events[dependentKey]

const events =
// If undefined, we haven't tried to fetch them yet. If not undefined,
// either they exist or they don't (null).
cachedEvents !== undefined
? ((cachedEvents ?? []) as GovStateEvent[])
: await GovStateEvent.findAll({
attributes: [
// DISTINCT ON is not directly supported by Sequelize, so we need
// to cast to unknown and back to string to insert this at the
// beginning of the query. This ensures we use the most recent
// version of each denom.
Sequelize.literal(
'DISTINCT ON("proposalId") \'\''
) as unknown as string,
'proposalId',
'blockHeight',
'blockTimeUnixMs',
'value',
],
where: {
blockHeight: blockHeightFilter,
},
order: [
// Needs to be first so we can use DISTINCT ON.
['proposalId', 'ASC'],
['blockHeight', 'DESC'],
],
})

// Type-check. Should never happen assuming dependent key namespaces are
// unique across different event types.
if (events.some((event) => !(event instanceof GovStateEvent))) {
throw new Error('Incorrect event type.')
}

// Cache events, null if nonexistent.
if (cachedEvents === undefined) {
cache.events[dependentKey] = events.length ? events : null
}

// If no events found, return undefined.
if (!events.length) {
return
}

// Call hook.
await onFetch?.(events)

// Create denom balance map.
return events.reduce(
(acc, { proposalId, value }) => ({
...acc,
[proposalId]: value,
}),
{} as Record<string, Record<string, any>>
)
}

return {
chainId,
block,
Expand Down Expand Up @@ -1119,5 +1238,8 @@ export const getEnv = ({

getBalance,
getBalances,

getProposal,
getProposals,
}
}
18 changes: 18 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ export type FormulaBalancesGetter = (
address: string
) => Promise<Record<string, string> | undefined>

export type FormulaProposalGetter = (
proposalId: string
) => Promise<Record<string, any> | undefined>

export type FormulaProposalsGetter = () => Promise<
Record<string, Record<string, any>> | undefined
>

export type Env<Args extends Record<string, string> = {}> = {
chainId: string
block: Block
Expand Down Expand Up @@ -271,6 +279,8 @@ export type Env<Args extends Record<string, string> = {}> = {
getTxEvents: FormulaTxEventsGetter
getBalance: FormulaBalanceGetter
getBalances: FormulaBalancesGetter
getProposal: FormulaProposalGetter
getProposals: FormulaProposalsGetter
}

export interface EnvOptions {
Expand Down Expand Up @@ -466,6 +476,14 @@ export type ParsedBankStateEvent = {
balance: string
}

export type ParsedGovStateEvent = {
proposalId: string
blockHeight: string
blockTimeUnixMs: string
blockTimestamp: Date
value: Record<string, any>
}

type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
T,
Exclude<keyof T, Keys>
Expand Down
25 changes: 22 additions & 3 deletions src/core/utils/block.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Op } from 'sequelize'

import { BankStateEvent, WasmStateEvent } from '@/db'
import { BankStateEvent, GovStateEvent, WasmStateEvent } from '@/db'

import { Block } from '../types'

export const getBlockForTime = async (
blockTimeUnixMs: bigint
): Promise<Block | undefined> => {
const [wasmEvent, bankEvent] = await Promise.all([
const [wasmEvent, bankEvent, govEvent] = await Promise.all([
await WasmStateEvent.findOne({
where: {
blockTimeUnixMs: {
Expand All @@ -26,17 +26,27 @@ export const getBlockForTime = async (
},
order: [['blockTimeUnixMs', 'DESC']],
}),
await GovStateEvent.findOne({
where: {
blockTimeUnixMs: {
[Op.gt]: 0,
[Op.lte]: blockTimeUnixMs,
},
},
order: [['blockTimeUnixMs', 'DESC']],
}),
])

// Choose latest block.
return [
...(wasmEvent ? [wasmEvent.block] : []),
...(bankEvent ? [bankEvent.block] : []),
...(govEvent ? [govEvent.block] : []),
].sort((a, b) => Number(b.height - a.height))[0]
}

export const getFirstBlock = async (): Promise<Block | undefined> => {
const [wasmEvent, bankEvent] = await Promise.all([
const [wasmEvent, bankEvent, govEvent] = await Promise.all([
await WasmStateEvent.findOne({
where: {
blockTimeUnixMs: {
Expand All @@ -53,11 +63,20 @@ export const getFirstBlock = async (): Promise<Block | undefined> => {
},
order: [['blockTimeUnixMs', 'ASC']],
}),
await GovStateEvent.findOne({
where: {
blockTimeUnixMs: {
[Op.gt]: 0,
},
},
order: [['blockTimeUnixMs', 'ASC']],
}),
])

// Choose latest block.
return [
...(wasmEvent ? [wasmEvent.block] : []),
...(bankEvent ? [bankEvent.block] : []),
...(govEvent ? [govEvent.block] : []),
].sort((a, b) => Number(b.height - a.height))[0]
}
2 changes: 1 addition & 1 deletion src/core/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const getDependentKey = (
namespace: DependentKeyNamespace,
// If empty/undefined, wildcard used.
...keys: (string | undefined)[]
) => `${namespace}:${keys.map((key) => key || '*').join(':')}`
) => `${[namespace, ...keys].map((key) => key || '*').join(':')}`

export const validateBlockString = (block: string, subject: string): Block => {
let parsedBlock
Expand Down
88 changes: 88 additions & 0 deletions src/data/formulas/generic/gov.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { GenericFormula } from '@/core'

export const proposal: GenericFormula<
Record<string, any> | undefined,
{ proposalId: string }
> = {
compute: async ({ getProposal, args: { proposalId } }) => {
if (!proposalId) {
throw new Error('missing `proposalId`')
}

return await getProposal(proposalId)
},
}

export const proposals: GenericFormula<
{
proposals: Record<string, any>[]
total: number
},
{
offset?: string
limit?: string
}
> = {
compute: async ({ getProposals, args: { offset, limit } }) => {
const offsetNum = offset ? Math.max(0, Number(offset)) : 0
const limitNum = limit ? Math.max(0, Number(limit)) : Infinity

if (isNaN(offsetNum)) {
throw new Error('invalid `offset`')
}
if (isNaN(limitNum)) {
throw new Error('invalid `limit`')
}

const proposals = (await getProposals()) || {}

// Sort ascending.
const proposalIds = Object.keys(proposals).sort((a, b) =>
Number(BigInt(a) - BigInt(b))
)

return {
proposals: proposalIds
.slice(offsetNum, offsetNum + limitNum)
.map((proposalId) => proposals[proposalId]),
total: proposalIds.length,
}
},
}

export const reverseProposals: GenericFormula<
{
proposals: Record<string, any>[]
total: number
},
{
offset?: string
limit?: string
}
> = {
compute: async ({ getProposals, args: { offset, limit } }) => {
const offsetNum = offset ? Math.max(0, Number(offset)) : 0
const limitNum = limit ? Math.max(0, Number(limit)) : Infinity

if (isNaN(offsetNum)) {
throw new Error('invalid `offset`')
}
if (isNaN(limitNum)) {
throw new Error('invalid `limit`')
}

const proposals = (await getProposals()) || {}

// Sort descending.
const proposalIds = Object.keys(proposals).sort((a, b) =>
Number(BigInt(b) - BigInt(a))
)

return {
proposals: proposalIds
.slice(offsetNum, offsetNum + limitNum)
.map((proposalId) => proposals[proposalId]),
total: proposalIds.length,
}
},
}
1 change: 1 addition & 0 deletions src/data/formulas/generic/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './priority_featured_daos'
export * as gov from './gov'
export * as token from './token'
2 changes: 2 additions & 0 deletions src/db/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Computation,
ComputationDependency,
Contract,
GovStateEvent,
StakingSlashEvent,
State,
Validator,
Expand All @@ -40,6 +41,7 @@ const getModelsForType = (type: DbType): SequelizeOptions['models'] =>
Computation,
ComputationDependency,
Contract,
GovStateEvent,
StakingSlashEvent,
State,
Validator,
Expand Down
Loading

0 comments on commit 91db149

Please sign in to comment.