Skip to content
This repository was archived by the owner on May 11, 2026. It is now read-only.

Commit db2a9a6

Browse files
wa0x6eclaude
andcommitted
feat: add vp_value to leaderboard and defer vote deletion to async script
- Add vp_value column to leaderboard table, updated asynchronously by votesVpValue when votes are finalized - Add PENDING_DELETE cb status for deferred vote deletion - Add deleteProposalVotes async script to process flagged votes in batches (update leaderboard/spaces counters, then delete) - Simplify delete-proposal writer: flags votes instead of deleting inline - Initialize vp_value to 0 on leaderboard row creation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d412b03 commit db2a9a6

7 files changed

Lines changed: 130 additions & 28 deletions

File tree

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const CB = {
33
PENDING_SYNC: 0, // Default db value, waiting from value from overlord
44
PENDING_COMPUTE: -1,
55
PENDING_FINAL: -2,
6+
PENDING_DELETE: -3,
67
INELIGIBLE: -10, // Payload format, can not compute
78
ERROR_SYNC: -11 // Sync error from overlord, waiting for retry
89
};

src/helpers/deleteProposalVotes.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import snapshot from '@snapshot-labs/snapshot.js';
2+
import log from './log';
3+
import db from './mysql';
4+
import { CB } from '../constants';
5+
6+
type Vote = {
7+
id: string;
8+
voter: string;
9+
space: string;
10+
vp_value: number;
11+
vp_state: string;
12+
};
13+
14+
const REFRESH_INTERVAL = 60 * 1000;
15+
const BATCH_SIZE = 100;
16+
17+
async function getVotesPendingDeletion(): Promise<Vote[]> {
18+
return db.queryAsync(
19+
`SELECT id, voter, space, vp_value, vp_state FROM votes WHERE cb = ? LIMIT ?`,
20+
[CB.PENDING_DELETE, BATCH_SIZE]
21+
);
22+
}
23+
24+
async function processVotes(votes: Vote[]) {
25+
const query: string[] = [];
26+
const params: (string | number | string[])[] = [];
27+
28+
// Update spaces vote_count
29+
const grouped = new Map<string, Vote[]>();
30+
for (const vote of votes) {
31+
if (!grouped.has(vote.space)) {
32+
grouped.set(vote.space, []);
33+
}
34+
grouped.get(vote.space)!.push(vote);
35+
}
36+
37+
for (const [space, spaceVotes] of grouped) {
38+
query.push('UPDATE spaces SET vote_count = GREATEST(vote_count - ?, 0) WHERE id = ?');
39+
params.push(spaceVotes.length, space);
40+
}
41+
42+
// Update leaderboard vote_count and vp_value
43+
for (const vote of votes) {
44+
query.push(
45+
`UPDATE leaderboard
46+
SET vote_count = GREATEST(vote_count - 1, 0),
47+
vp_value = GREATEST(vp_value - ?, 0)
48+
WHERE user = ? AND space = ?`
49+
);
50+
params.push(vote.vp_state === 'final' ? vote.vp_value : 0, vote.voter, vote.space);
51+
}
52+
53+
// Delete votes
54+
query.push('DELETE FROM votes WHERE id IN (?)');
55+
params.push(votes.map(v => v.id));
56+
57+
await db.queryAsync(query.join(';'), params);
58+
}
59+
60+
export default async function run() {
61+
while (true) {
62+
const votes = await getVotesPendingDeletion();
63+
64+
if (votes.length) {
65+
log.info(`[deleteProposalVotes] ${votes.length} votes to delete`);
66+
await processVotes(votes);
67+
}
68+
69+
if (votes.length < BATCH_SIZE) {
70+
log.info('[deleteProposalVotes] sleeping');
71+
await snapshot.utils.sleep(REFRESH_INTERVAL);
72+
}
73+
}
74+
}

src/helpers/votesVpValue.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ type ProposalVpValues = Map<string, { cb: number; vpValueByStrategy: number[] }>
99

1010
type Datum = {
1111
id: string;
12+
voter: string;
13+
space: string;
1214
vpState: string;
1315
vpByStrategy: number[];
1416
vpValueByStrategy: number[];
@@ -22,6 +24,8 @@ const PROPOSALS_BATCH_SIZE = 50000;
2224
const datumSchema = z
2325
.object({
2426
id: z.string(),
27+
voter: z.string(),
28+
space: z.string(),
2529
vpState: z.string(),
2630
vpValueByStrategy: z.array(z.number().finite()),
2731
vpByStrategy: z.array(z.number().finite())
@@ -31,17 +35,26 @@ const datumSchema = z
3135
});
3236

3337
async function getPendingVotes(): Promise<
34-
{ id: string; proposal: string; vpState: string; vpByStrategy: number[] }[]
38+
{
39+
id: string;
40+
voter: string;
41+
space: string;
42+
proposal: string;
43+
vpState: string;
44+
vpByStrategy: number[];
45+
}[]
3546
> {
3647
const query = `
37-
SELECT id, proposal, vp_state, vp_by_strategy
48+
SELECT id, voter, space, proposal, vp_state, vp_by_strategy
3849
FROM votes
3950
WHERE cb = ?
4051
LIMIT ?`;
4152
const results = await db.queryAsync(query, [CB.PENDING_COMPUTE, DEFAULT_BATCH_SIZE]);
4253

4354
return results.map((r: any) => ({
4455
id: r.id,
56+
voter: r.voter,
57+
space: r.space,
4558
proposal: r.proposal,
4659
vpState: r.vp_state,
4760
vpByStrategy: JSON.parse(r.vp_by_strategy)
@@ -88,6 +101,7 @@ async function refreshVotesVpValues(data: Datum[]) {
88101
const ids: string[] = [];
89102
const vpValues: Map<string, number> = new Map();
90103
const cbValues: Map<string, number> = new Map();
104+
const leaderboardUpdates: { value: number; voter: string; space: string }[] = [];
91105

92106
for (const datum of data) {
93107
if (datum.proposalCb === CB.INELIGIBLE) {
@@ -110,6 +124,16 @@ async function refreshVotesVpValues(data: Datum[]) {
110124
validatedDatum.id,
111125
validatedDatum.vpState === 'final' ? CB.FINAL : CB.PENDING_FINAL
112126
);
127+
128+
// Leaderboard update only for final votes
129+
// to avoid vp fluctuations with overriding strategies
130+
if (validatedDatum.vpState === 'final') {
131+
leaderboardUpdates.push({
132+
value,
133+
voter: validatedDatum.voter,
134+
space: validatedDatum.space
135+
});
136+
}
113137
} catch (e) {
114138
capture(e);
115139
ids.push(datum.id);
@@ -132,8 +156,21 @@ async function refreshVotesVpValues(data: Datum[]) {
132156
cbParams.push(id, cbValues.get(id)!);
133157
}
134158

135-
const query = `UPDATE votes SET vp_value = CASE ${vpCases} END, cb = CASE ${cbCases} END WHERE id IN (${placeholders})`;
136-
await db.queryAsync(query, [...vpParams, ...cbParams, ...ids]);
159+
const queries: string[] = [
160+
`UPDATE votes SET vp_value = CASE ${vpCases} END, cb = CASE ${cbCases} END WHERE id IN (${placeholders})`
161+
];
162+
const params: (number | string)[] = [...vpParams, ...cbParams, ...ids];
163+
164+
for (const update of leaderboardUpdates) {
165+
queries.push(
166+
`UPDATE leaderboard
167+
SET vp_value = vp_value + ?
168+
WHERE user = ? AND space = ?`
169+
);
170+
params.push(update.value, update.voter, update.space);
171+
}
172+
173+
await db.queryAsync(queries.join(';'), params);
137174
}
138175

139176
async function processBatch(proposalVpValues: ProposalVpValues): Promise<number> {
@@ -146,6 +183,8 @@ async function processBatch(proposalVpValues: ProposalVpValues): Promise<number>
146183
const proposal = proposalVpValues.get(v.proposal)!;
147184
return {
148185
id: v.id,
186+
voter: v.voter,
187+
space: v.space,
149188
vpState: v.vpState,
150189
vpByStrategy: v.vpByStrategy,
151190
vpValueByStrategy: proposal.vpValueByStrategy,

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { fallbackLogger, initLogger } from '@snapshot-labs/snapshot-sentry';
33
import cors from 'cors';
44
import express from 'express';
55
import api from './api';
6+
import deleteProposalVotes from './helpers/deleteProposalVotes';
67
import log from './helpers/log';
78
import initMetrics from './helpers/metrics';
89
import refreshModeration from './helpers/moderation';
@@ -26,6 +27,7 @@ async function startServer() {
2627
refreshProposalsVpValue();
2728
refreshProposalsScoresValue();
2829
refreshVotesVpValue();
30+
deleteProposalVotes();
2931

3032
await initializeStrategies();
3133
refreshStrategies();

src/writer/delete-proposal.ts

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CB } from '../constants';
12
import { getProposal, getSpace } from '../helpers/actions';
23
import db from '../helpers/mysql';
34
import { jsonParse } from '../helpers/utils';
@@ -21,36 +22,19 @@ export async function verify(body): Promise<any> {
2122
export async function action(body): Promise<void> {
2223
const msg = jsonParse(body.msg);
2324
const proposal = await getProposal(msg.space, msg.payload.proposal);
24-
25-
const voters = await db.queryAsync(`SELECT voter FROM votes WHERE proposal = ?`, [
26-
msg.payload.proposal
27-
]);
2825
const id = msg.payload.proposal;
2926

30-
let queries = `
27+
const queries = `
3128
DELETE FROM proposals WHERE id = ? LIMIT 1;
32-
DELETE FROM votes WHERE proposal = ?;
29+
UPDATE votes SET cb = ? WHERE proposal = ?;
3330
UPDATE leaderboard
3431
SET proposal_count = GREATEST(proposal_count - 1, 0)
3532
WHERE user = ? AND space = ?
3633
LIMIT 1;
3734
UPDATE spaces
38-
SET proposal_count = GREATEST(proposal_count - 1, 0), vote_count = GREATEST(vote_count - ?, 0)
35+
SET proposal_count = GREATEST(proposal_count - 1, 0)
3936
WHERE id = ?;
4037
`;
4138

42-
const parameters = [id, id, proposal.author, msg.space, voters.length, msg.space];
43-
44-
if (voters.length > 0) {
45-
queries += `
46-
UPDATE leaderboard SET vote_count = GREATEST(vote_count - 1, 0)
47-
WHERE user IN (?) AND space = ?;
48-
`;
49-
parameters.push(
50-
voters.map(voter => voter.voter),
51-
msg.space
52-
);
53-
}
54-
55-
await db.queryAsync(queries, parameters);
39+
await db.queryAsync(queries, [id, CB.PENDING_DELETE, id, proposal.author, msg.space, msg.space]);
5640
}

src/writer/vote.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ export async function action(body, ipfs, receipt, id, context): Promise<void> {
182182
await db.queryAsync(
183183
`
184184
INSERT INTO votes SET ?;
185-
INSERT INTO leaderboard (space, user, vote_count, last_vote)
186-
VALUES(?, ?, 1, ?)
185+
INSERT INTO leaderboard (space, user, vote_count, last_vote, vp_value)
186+
VALUES(?, ?, 1, ?, 0)
187187
ON DUPLICATE KEY UPDATE vote_count = vote_count + 1, last_vote = ?;
188188
UPDATE spaces SET vote_count = vote_count + 1 WHERE id = ?;
189189
`,

test/schema.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,13 @@ CREATE TABLE leaderboard (
192192
vote_count SMALLINT UNSIGNED NOT NULL DEFAULT '0',
193193
proposal_count SMALLINT UNSIGNED NOT NULL DEFAULT '0',
194194
last_vote BIGINT,
195+
vp_value DECIMAL(13,3) NOT NULL DEFAULT 0.000,
195196
PRIMARY KEY user_space (user,space),
196197
INDEX space (space),
197198
INDEX vote_count (vote_count),
198199
INDEX proposal_count (proposal_count),
199-
INDEX last_vote (last_vote)
200+
INDEX last_vote (last_vote),
201+
INDEX vp_value (vp_value)
200202
);
201203

202204
CREATE TABLE skins (

0 commit comments

Comments
 (0)