diff --git a/src/helpers/entityValue.ts b/src/helpers/entityValue.ts index 765ce1eb..b7bb22c9 100644 --- a/src/helpers/entityValue.ts +++ b/src/helpers/entityValue.ts @@ -1,9 +1,5 @@ import { jsonRpcRequest } from './utils'; -type Vote = { - vp_by_strategy: number[]; -}; - type Proposal = { network: string; strategies: any[]; @@ -28,20 +24,3 @@ export async function getVpValueByStrategy(proposal: Proposal): Promise parseFloat(value.toFixed(STRATEGIES_VALUE_PRECISION))); } - -/** - * Calculates the total vote value based on the voting power and the proposal's value per strategy. - * @returns The total vote value, in the currency unit specified by the proposal's vp_value_by_strategy values - **/ -export function getVoteValue(proposal: { vp_value_by_strategy: number[] }, vote: Vote): number { - if (!proposal.vp_value_by_strategy.length) return 0; - - if (proposal.vp_value_by_strategy.length !== vote.vp_by_strategy.length) { - throw new Error('invalid data to compute vote value'); - } - - return proposal.vp_value_by_strategy.reduce( - (sum, value, index) => sum + value * vote.vp_by_strategy[index], - 0 - ); -} diff --git a/src/helpers/votesVpValue.ts b/src/helpers/votesVpValue.ts new file mode 100644 index 00000000..e02ea13d --- /dev/null +++ b/src/helpers/votesVpValue.ts @@ -0,0 +1,94 @@ +import { capture } from '@snapshot-labs/snapshot-sentry'; +import snapshot from '@snapshot-labs/snapshot.js'; +import { z } from 'zod'; +import db from './mysql'; +import { CB } from '../constants'; + +type Datum = { + id: string; + vpState: string; + vpByStrategy: number[]; + vpValueByStrategy: number[]; +}; + +const REFRESH_INTERVAL = 10 * 1000; +const BATCH_SIZE = 100; + +const datumSchema = z + .object({ + id: z.string(), + vpState: z.string(), + vpValueByStrategy: z.array(z.number().finite()), + vpByStrategy: z.array(z.number().finite()) + }) + .refine(data => data.vpValueByStrategy.length === data.vpByStrategy.length, { + message: 'Array length mismatch: vpValueByStrategy and vpByStrategy must have the same length' + }); + +async function getVotes(): Promise { + const query = ` + SELECT votes.id, votes.vp_state, votes.vp_by_strategy, proposals.vp_value_by_strategy + FROM votes + JOIN proposals ON votes.proposal = proposals.id + WHERE proposals.cb IN (?) AND votes.cb = ? + ORDER BY votes.created ASC + LIMIT ?`; + const results = await db.queryAsync(query, [ + [CB.PENDING_FINAL, CB.PENDING_COMPUTE, CB.FINAL], + CB.PENDING_COMPUTE, + BATCH_SIZE + ]); + + return results.map((r: any) => { + return { + id: r.id, + vpState: r.vp_state, + vpValueByStrategy: JSON.parse(r.vp_value_by_strategy), + vpByStrategy: JSON.parse(r.vp_by_strategy) + }; + }); +} + +async function refreshVotesVpValues(data: Datum[]) { + const query: string[] = []; + const params: any[] = []; + + for (const datum of data) { + query.push('UPDATE votes SET vp_value = ?, cb = ? WHERE id = ? LIMIT 1'); + + try { + const validatedDatum = datumSchema.parse(datum); + const value = validatedDatum.vpValueByStrategy.reduce( + (sum, value, index) => sum + value * validatedDatum.vpByStrategy[index], + 0 + ); + + params.push( + value, + validatedDatum.vpState === 'final' ? CB.FINAL : CB.PENDING_FINAL, + validatedDatum.id + ); + } catch (e) { + capture(e); + params.push(0, CB.INELIGIBLE, datum.id); + } + } + + if (query.length) { + await db.queryAsync(query.join(';'), params); + } +} + +export default async function run() { + while (true) { + const votes = await getVotes(); + + if (votes.length) { + await refreshVotesVpValues(votes); + } + + if (votes.length < BATCH_SIZE) { + await snapshot.utils.sleep(REFRESH_INTERVAL); + } + } +} diff --git a/src/index.ts b/src/index.ts index 22347aca..bfbaeeec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { stop as stopStrategies } from './helpers/strategies'; import { trackTurboStatuses } from './helpers/turbo'; +import refreshVotesVpValue from './helpers/votesVpValue'; const app = express(); @@ -24,6 +25,7 @@ async function startServer() { refreshModeration(); refreshProposalsVpValue(); refreshProposalsScoresValue(); + refreshVotesVpValue(); await initializeStrategies(); refreshStrategies(); diff --git a/src/scores.ts b/src/scores.ts index dfa51974..34f18dfb 100644 --- a/src/scores.ts +++ b/src/scores.ts @@ -1,6 +1,5 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { CB } from './constants'; -import { getVoteValue } from './helpers/entityValue'; import log from './helpers/log'; import db from './helpers/mysql'; import { getDecryptionKey } from './helpers/shutter'; @@ -62,12 +61,13 @@ async function updateVotesVp(votes: any[], vpState: string, proposalId: string) let query = ''; votesInPage.forEach((vote: any) => { query += `UPDATE votes - SET vp = ?, vp_by_strategy = ?, vp_state = ?, vp_value = ? + SET vp = ?, vp_by_strategy = ?, vp_state = ?, vp_value = ?, cb = ? WHERE id = ? AND proposal = ? LIMIT 1; `; params.push(vote.balance); params.push(JSON.stringify(vote.scores)); params.push(vpState); params.push(vote.vp_value); + params.push(CB.PENDING_COMPUTE); params.push(vote.id); params.push(proposalId); }); @@ -165,7 +165,6 @@ export async function updateProposalAndVotes(proposalId: string, force = false) votes = votes.map((vote: any) => { vote.scores = proposal.strategies.map((strategy, i) => scores[i][vote.voter] || 0); vote.balance = vote.scores.reduce((a, b: any) => a + b, 0); - vote.vp_value = getVoteValue(proposal, vote); return vote; }); } diff --git a/test/unit/helpers/entityValue.test.ts b/test/unit/helpers/entityValue.test.ts deleted file mode 100644 index 7dba57b1..00000000 --- a/test/unit/helpers/entityValue.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { getVoteValue } from '../../../src/helpers/entityValue'; - -describe('getVoteValue', () => { - it('should calculate correct vote value with single strategy', () => { - const proposal = { vp_value_by_strategy: [2.5] }; - const vote = { vp_by_strategy: [100] }; - - const result = getVoteValue(proposal, vote); - - expect(result).toBe(250); - }); - - it('should calculate correct vote value with multiple strategies', () => { - const proposal = { vp_value_by_strategy: [1.5, 3.0, 0.5] }; - const vote = { vp_by_strategy: [100, 50, 200] }; - - const result = getVoteValue(proposal, vote); - - expect(result).toBe(400); // (1.5 * 100) + (3.0 * 50) + (0.5 * 200) = 150 + 150 + 100 = 400 - }); - - it('should return 0 when vote has no voting power', () => { - const proposal = { vp_value_by_strategy: [2.0, 1.5] }; - const vote = { vp_by_strategy: [0, 0] }; - - const result = getVoteValue(proposal, vote); - - expect(result).toBe(0); - }); - - it('should return 0 when proposal has no value per strategy', () => { - const proposal = { vp_value_by_strategy: [0, 0] }; - const vote = { vp_by_strategy: [100, 50] }; - - const result = getVoteValue(proposal, vote); - - expect(result).toBe(0); - }); - - it('should handle decimal values correctly', () => { - const proposal = { vp_value_by_strategy: [0.1, 0.25] }; - const vote = { vp_by_strategy: [10, 20] }; - - const result = getVoteValue(proposal, vote); - - expect(result).toBe(6); // (0.1 * 10) + (0.25 * 20) = 1 + 5 = 6 - }); - - it('should throw error when strategy arrays have different lengths', () => { - const proposal = { vp_value_by_strategy: [1.0, 2.0] }; - const vote = { vp_by_strategy: [100] }; - - expect(() => getVoteValue(proposal, vote)).toThrow('invalid data to compute vote value'); - }); - - it('should throw error when vote has more strategies than proposal', () => { - const proposal = { vp_value_by_strategy: [1.0] }; - const vote = { vp_by_strategy: [100, 50] }; - - expect(() => getVoteValue(proposal, vote)).toThrow('invalid data to compute vote value'); - }); - - it('should handle empty arrays', () => { - const proposal = { vp_value_by_strategy: [] }; - const vote = { vp_by_strategy: [] }; - - const result = getVoteValue(proposal, vote); - - expect(result).toBe(0); - }); -});