Skip to content

Commit

Permalink
index gov proposal votes
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Aug 16, 2024
1 parent ff0ad71 commit b991e8f
Show file tree
Hide file tree
Showing 23 changed files with 1,386 additions and 103 deletions.
6 changes: 4 additions & 2 deletions src/db/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
ComputationDependency,
Contract,
DistributionCommunityPoolStateEvent,
GovStateEvent,
GovProposal,
GovProposalVote,
StakingSlashEvent,
State,
Validator,
Expand Down Expand Up @@ -45,7 +46,8 @@ const getModelsForType = (type: DbType): SequelizeOptions['models'] =>
ComputationDependency,
Contract,
DistributionCommunityPoolStateEvent,
GovStateEvent,
GovProposal,
GovProposalVote,
StakingSlashEvent,
State,
Validator,
Expand Down
6 changes: 4 additions & 2 deletions src/db/dependable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { ComputationDependentKey, DependableEventModel } from '@/types'
import {
BankStateEvent,
DistributionCommunityPoolStateEvent,
GovStateEvent,
GovProposal,
GovProposalVote,
StakingSlashEvent,
WasmStateEvent,
WasmStateEventTransformation,
Expand All @@ -16,7 +17,8 @@ export const getDependableEventModels = (): typeof DependableEventModel[] => [
WasmTxEvent,
StakingSlashEvent,
BankStateEvent,
GovStateEvent,
GovProposal,
GovProposalVote,
DistributionCommunityPoolStateEvent,
]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { QueryInterface } from 'sequelize'

module.exports = {
async up(queryInterface: QueryInterface) {
await queryInterface.renameTable('GovStateEvents', 'GovProposals')
},

async down(queryInterface: QueryInterface) {
await queryInterface.renameTable('GovProposals', 'GovStateEvents')
},
}
67 changes: 67 additions & 0 deletions src/db/migrations/20240816165030-create-gov-proposal-vote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { QueryInterface, fn } from 'sequelize'
import { DataType } from 'sequelize-typescript'

module.exports = {
async up(queryInterface: QueryInterface) {
await queryInterface.createTable('GovProposalVotes', {
id: {
primaryKey: true,
autoIncrement: true,
type: DataType.INTEGER,
},
proposalId: {
allowNull: false,
type: DataType.BIGINT,
},
voterAddress: {
allowNull: false,
type: DataType.STRING,
},
blockHeight: {
allowNull: false,
type: DataType.BIGINT,
},
blockTimeUnixMs: {
allowNull: false,
type: DataType.BIGINT,
},
blockTimestamp: {
allowNull: false,
type: DataType.DATE,
},
data: {
allowNull: false,
type: DataType.TEXT,
},
createdAt: {
allowNull: false,
type: DataType.DATE,
defaultValue: fn('NOW'),
},
updatedAt: {
allowNull: false,
type: DataType.DATE,
defaultValue: fn('NOW'),
},
})
await queryInterface.addIndex('GovProposalVotes', {
unique: true,
fields: ['blockHeight', 'proposalId', 'voterAddress'],
})
await queryInterface.addIndex('GovProposalVotes', {
fields: ['proposalId'],
})
await queryInterface.addIndex('GovProposalVotes', {
fields: ['voterAddress'],
})
await queryInterface.addIndex('GovProposalVotes', {
fields: ['blockHeight'],
})
await queryInterface.addIndex('GovProposalVotes', {
fields: ['blockTimeUnixMs'],
})
},
async down(queryInterface: QueryInterface) {
await queryInterface.dropTable('GovProposalVotes')
},
}
4 changes: 3 additions & 1 deletion src/db/models/Computation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ export class Computation extends Model {
await Promise.all(
getDependableEventModels().map(async (DependableEventModel) => {
const namespacedKeys = this.dependencies.filter(({ key }) =>
key.startsWith(DependableEventModel.dependentKeyNamespace)
key.startsWith(
DependableEventModel.dependentKeyNamespace + ':'
)
)
if (namespacedKeys.length === 0) {
return null
Expand Down
12 changes: 6 additions & 6 deletions src/db/models/GovStateEvent.ts → src/db/models/GovProposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { getDependentKey } from '@/utils'
},
],
})
export class GovStateEvent extends DependableEventModel {
export class GovProposal extends DependableEventModel {
@AllowNull(false)
@Column(DataType.BIGINT)
declare proposalId: string
Expand Down Expand Up @@ -64,16 +64,16 @@ export class GovStateEvent extends DependableEventModel {
}

get dependentKey(): string {
return getDependentKey(GovStateEvent.dependentKeyNamespace, this.proposalId)
return getDependentKey(GovProposal.dependentKeyNamespace, this.proposalId)
}

// Get the previous event for this proposalId. If this is the first event for
// this proposalId, return null. Cache the result so it can be reused since
// this shouldn't change.
previousEvent?: GovStateEvent | null
async getPreviousEvent(cache = true): Promise<GovStateEvent | null> {
previousEvent?: GovProposal | null
async getPreviousEvent(cache = true): Promise<GovProposal | null> {
if (this.previousEvent === undefined || !cache) {
this.previousEvent = await GovStateEvent.findOne({
this.previousEvent = await GovProposal.findOne({
where: {
proposalId: this.proposalId,
blockHeight: {
Expand All @@ -87,7 +87,7 @@ export class GovStateEvent extends DependableEventModel {
return this.previousEvent
}

static dependentKeyNamespace = DependentKeyNamespace.GovStateEvent
static dependentKeyNamespace = DependentKeyNamespace.GovProposal
static blockHeightKey: string = 'blockHeight'
static blockTimeUnixMsKey: string = 'blockTimeUnixMs'

Expand Down
178 changes: 178 additions & 0 deletions src/db/models/GovProposalVote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Op, WhereOptions } from 'sequelize'
import { AllowNull, Column, DataType, Table } from 'sequelize-typescript'

import {
Block,
ComputationDependentKey,
DependableEventModel,
DependentKeyNamespace,
} from '@/types'
import { getDependentKey } from '@/utils'

@Table({
timestamps: true,
indexes: [
// Only one vote can be cast for a proposal ID by a voter at a given block
// height. This ensures events are not duplicated if they attempt exporting
// multiple times.
{
unique: true,
fields: ['blockHeight', 'proposalId', 'voterAddress'],
},
{
fields: ['proposalId'],
},
{
fields: ['voterAddress'],
},
{
// Speed up ordering queries.
fields: ['blockHeight'],
},
{
// Speed up ordering queries.
fields: ['blockTimeUnixMs'],
},
],
})
export class GovProposalVote extends DependableEventModel {
@AllowNull(false)
@Column(DataType.BIGINT)
declare proposalId: string

@AllowNull(false)
@Column(DataType.STRING)
declare voterAddress: string

@AllowNull(false)
@Column(DataType.BIGINT)
declare blockHeight: string

@AllowNull(false)
@Column(DataType.BIGINT)
declare blockTimeUnixMs: string

@AllowNull(false)
@Column(DataType.DATE)
declare blockTimestamp: Date

// Base64-encoded protobuf data.
@AllowNull(false)
@Column(DataType.TEXT)
declare data: string

get block(): Block {
return {
height: BigInt(this.blockHeight),
timeUnixMs: BigInt(this.blockTimeUnixMs),
}
}

get dependentKey(): string {
return getDependentKey(
GovProposalVote.dependentKeyNamespace,
this.proposalId,
this.voterAddress
)
}

// Get the previous event for this proposalId. If this is the first event for
// this proposalId, return null. Cache the result so it can be reused since
// this shouldn't change.
previousEvent?: GovProposalVote | null
async getPreviousEvent(cache = true): Promise<GovProposalVote | null> {
if (this.previousEvent === undefined || !cache) {
this.previousEvent = await GovProposalVote.findOne({
where: {
proposalId: this.proposalId,
voterAddress: this.voterAddress,
blockHeight: {
[Op.lt]: this.blockHeight,
},
},
order: [['blockHeight', 'DESC']],
})
}

return this.previousEvent
}

static dependentKeyNamespace = DependentKeyNamespace.GovProposalVote
static blockHeightKey: string = 'blockHeight'
static blockTimeUnixMsKey: string = 'blockTimeUnixMs'

// Returns a where clause that will match all events that are described by the
// dependent keys.
static getWhereClauseForDependentKeys(
dependentKeys: ComputationDependentKey[]
): WhereOptions {
const dependentKeysByProposalId = dependentKeys.reduce(
(acc, dependentKey) => {
// 1. Remove namespace from key.
const key = dependentKey.key.replace(
new RegExp(`^${this.dependentKeyNamespace}:`),
''
)

// 2. Extract proposalId from key.
// Dependent keys for any proposal start with "*:".
const proposalId = key.startsWith('*:') ? '' : key.split(':')[0]

const voterAddress = key
// 3. Remove proposalId from key.
.replace(new RegExp(`^${proposalId || '\\*'}:`), '')
// 4. Replace wildcard symbol with LIKE wildcard for database query.
.replace(/\*/g, '%')

return {
...acc,
[proposalId]: [
...(acc[proposalId] ?? []),
{
voterAddress,
prefix: dependentKey.prefix,
},
],
}
},
{} as Record<string, { voterAddress: string; prefix: boolean }[]>
)

return {
[Op.or]: Object.entries(dependentKeysByProposalId).map(
([proposalId, keys]) => {
const exactKeys = keys
.filter(
({ voterAddress, prefix }) =>
!prefix && !voterAddress.includes('%')
)
.map(({ voterAddress }) => voterAddress)
const wildcardKeys = keys
.filter(
({ voterAddress, prefix }) => prefix || voterAddress.includes('%')
)
.map(
({ voterAddress, prefix }) => voterAddress + (prefix ? '%' : '')
)

return {
// Only include if proposalId is defined.
...(proposalId && { proposalId }),
// Related logic in `makeComputationDependencyWhere` in
// `src/db/computation.ts`.
voterAddress: {
[Op.or]: [
// Exact matches.
...(exactKeys.length > 0 ? [{ [Op.in]: exactKeys }] : []),
// Wildcards. May or may not be prefixes.
...wildcardKeys.map((voterAddress) => ({
[Op.like]: voterAddress,
})),
],
},
}
}
),
}
}
}
3 changes: 2 additions & 1 deletion src/db/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export * from './Computation'
export * from './ComputationDependency'
export * from './Contract'
export * from './DistributionCommunityPoolStateEvent'
export * from './GovStateEvent'
export * from './GovProposal'
export * from './GovProposalVote'
export * from './StakingSlashEvent'
export * from './State'
export * from './Validator'
Expand Down
2 changes: 1 addition & 1 deletion src/formulas/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export const computeRange = async ({
await Promise.all(
getDependableEventModels().map(async (DependableEventModel) => {
const namespacedDependentKeys = newDependentKeys.filter(({ key }) =>
key.startsWith(DependableEventModel.dependentKeyNamespace)
key.startsWith(DependableEventModel.dependentKeyNamespace + ':')
)
if (namespacedDependentKeys.length === 0) {
return []
Expand Down
Loading

0 comments on commit b991e8f

Please sign in to comment.