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

Commit 942261c

Browse files
authored
Merge branch 'master' into feat-pull-overriding-strategies-from-strategies-list
2 parents 230a0b0 + caa0625 commit 942261c

20 files changed

Lines changed: 252 additions & 62 deletions

docs/cb-flow.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
| -1 | `PENDING_COMPUTE` | Strategy values synced. Waiting for `scores_total_value` computation | Default. Waiting for `vp_value` computation |
99
| -2 | `PENDING_FINAL` | Score value computed, but proposal still active | Value computed, but `vp_state` not final |
1010
| 1 | `FINAL` | Fully computed | Fully computed |
11+
| -3 | `PENDING_DELETE` | N/A | Proposal deleted, vote awaiting async cleanup |
1112
| -10 | `INELIGIBLE` | Invalid payload format, cannot compute (permanent) | Invalid data, cannot compute (permanent) |
1213
| -11 | `ERROR_SYNC` | Overlord sync failed, will be retried | N/A |
1314

@@ -54,14 +55,23 @@ flowchart TD
5455
C -->|No| E["cb = -2<br>PENDING_FINAL"]
5556
E -->|New vote on same proposal<br>triggers scores.ts<br>vp recalculated| B
5657
58+
B -->|delete-proposal| H["cb = -3<br>PENDING_DELETE"]
59+
D -->|delete-proposal| H
60+
E -->|delete-proposal| H
61+
G -->|delete-proposal| H
62+
H -->|deleteProposalVotes.ts<br>async cleanup| I([Vote Deleted])
63+
5764
classDef user fill:#4a90d9,color:#fff
5865
classDef async fill:#e8833a,color:#fff
5966
6067
class A user
6168
class F,C async
62-
class B,D,E,G default
69+
class B,D,E,G,H default
70+
class I default
6371
6472
linkStyle 0 stroke:#4a90d9
6573
linkStyle 1,2,3,4,5 stroke:#e8833a
6674
linkStyle 6 stroke:#4a90d9
75+
linkStyle 7,8,9,10 stroke:#4a90d9
76+
linkStyle 11 stroke:#e8833a
6777
```

jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
export default {
7+
roots: ['<rootDir>/test'],
78
clearMocks: true,
89
collectCoverage: true,
910
collectCoverageFrom: ['./src/**'],

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.3",
44
"license": "MIT",
55
"scripts": {
6-
"lint": "eslint src/ --ext .ts",
6+
"lint": "eslint src/ test/ --ext .ts",
77
"lint:fix": "yarn lint --fix",
88
"typecheck": "tsc --noEmit",
99
"build": "tsc",

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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
};
11+
12+
const REFRESH_INTERVAL = 60 * 1000;
13+
const BATCH_SIZE = 100;
14+
15+
async function getVotesPendingDeletion(): Promise<Vote[]> {
16+
return db.queryAsync(`SELECT id, voter, space FROM votes WHERE cb = ? LIMIT ?`, [
17+
CB.PENDING_DELETE,
18+
BATCH_SIZE
19+
]);
20+
}
21+
22+
async function processVotes(votes: Vote[]) {
23+
const query: string[] = [];
24+
const params: (string | number | string[])[] = [];
25+
26+
// Update spaces vote_count
27+
const grouped = new Map<string, number>();
28+
for (const vote of votes) {
29+
grouped.set(vote.space, (grouped.get(vote.space) || 0) + 1);
30+
}
31+
for (const [space, count] of grouped) {
32+
query.push('UPDATE spaces SET vote_count = GREATEST(vote_count - ?, 0) WHERE id = ?');
33+
params.push(count, space);
34+
}
35+
36+
// Delete votes
37+
query.push('DELETE FROM votes WHERE id IN (?) AND cb = ?');
38+
params.push(
39+
votes.map(v => v.id),
40+
CB.PENDING_DELETE
41+
);
42+
43+
// Refresh leaderboard from remaining votes (idempotent)
44+
const pairs = new Set(votes.map(v => `${v.voter}:${v.space}`));
45+
if (pairs.size) {
46+
const pairPlaceholders = Array.from(pairs)
47+
.map(() => '(?, ?)')
48+
.join(', ');
49+
query.push(
50+
`UPDATE leaderboard l
51+
SET vote_count = (SELECT COUNT(*) FROM votes v WHERE v.voter = l.user AND v.space = l.space),
52+
vp_value = COALESCE((
53+
SELECT SUM(v.vp_value) FROM votes v WHERE v.voter = l.user AND v.space = l.space
54+
), 0)
55+
WHERE (l.user, l.space) IN (${pairPlaceholders})`
56+
);
57+
for (const pair of pairs) {
58+
const [voter, space] = pair.split(':');
59+
params.push(voter, space);
60+
}
61+
}
62+
63+
await db.queryAsync(query.join(';'), params);
64+
}
65+
66+
export default async function run() {
67+
while (true) {
68+
const votes = await getVotesPendingDeletion();
69+
70+
if (votes.length) {
71+
log.info(`[deleteProposalVotes] ${votes.length} votes to delete`);
72+
await processVotes(votes);
73+
}
74+
75+
if (votes.length < BATCH_SIZE) {
76+
log.info('[deleteProposalVotes] sleeping');
77+
await snapshot.utils.sleep(REFRESH_INTERVAL);
78+
}
79+
}
80+
}

src/helpers/shutter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import express from 'express';
66
import log from './log';
77
import db from './mysql';
88
import { getIp, jsonParse, jsonRpcRequest, rpcError, rpcSuccess } from './utils';
9+
import { CB } from '../constants';
910
import { updateProposalAndVotes } from '../scores';
1011

1112
init().then(() => log.info('[shutter] init'));
@@ -58,8 +59,8 @@ export async function setProposalKey(params) {
5859
const ts = (Date.now() / 1e3).toFixed();
5960
if (!proposal || ts < proposal.end) return false;
6061

61-
query = 'SELECT id, choice FROM votes WHERE proposal = ?';
62-
const votes = await db.queryAsync(query, [proposal.id]);
62+
query = 'SELECT id, choice FROM votes WHERE proposal = ? AND cb != ?';
63+
const votes = await db.queryAsync(query, [proposal.id, CB.PENDING_DELETE]);
6364

6465
const sqlParams: string[] = [];
6566
let sqlQuery = '';

src/helpers/votesVpValue.ts

Lines changed: 44 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 leaderboardPairs: Set<string> = new Set();
91105

92106
for (const datum of data) {
93107
if (datum.proposalCb === CB.INELIGIBLE) {
@@ -110,6 +124,8 @@ async function refreshVotesVpValues(data: Datum[]) {
110124
validatedDatum.id,
111125
validatedDatum.vpState === 'final' ? CB.FINAL : CB.PENDING_FINAL
112126
);
127+
128+
leaderboardPairs.add(`${validatedDatum.voter}:${validatedDatum.space}`);
113129
} catch (e) {
114130
capture(e);
115131
ids.push(datum.id);
@@ -132,8 +148,30 @@ async function refreshVotesVpValues(data: Datum[]) {
132148
cbParams.push(id, cbValues.get(id)!);
133149
}
134150

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]);
151+
const queries: string[] = [
152+
`UPDATE votes SET vp_value = CASE ${vpCases} END, cb = CASE ${cbCases} END WHERE id IN (${placeholders}) AND cb = ?`
153+
];
154+
const params: (number | string)[] = [...vpParams, ...cbParams, ...ids, CB.PENDING_COMPUTE];
155+
156+
// Refresh leaderboard vp_value using SUM from votes table (idempotent)
157+
if (leaderboardPairs.size) {
158+
const pairPlaceholders = Array.from(leaderboardPairs)
159+
.map(() => '(?, ?)')
160+
.join(', ');
161+
queries.push(
162+
`UPDATE leaderboard l
163+
SET vp_value = COALESCE((
164+
SELECT SUM(v.vp_value) FROM votes v WHERE v.voter = l.user AND v.space = l.space
165+
), 0)
166+
WHERE (l.user, l.space) IN (${pairPlaceholders})`
167+
);
168+
for (const pair of leaderboardPairs) {
169+
const [voter, space] = pair.split(':');
170+
params.push(voter, space);
171+
}
172+
}
173+
174+
await db.queryAsync(queries.join(';'), params);
137175
}
138176

139177
async function processBatch(proposalVpValues: ProposalVpValues): Promise<number> {
@@ -146,6 +184,8 @@ async function processBatch(proposalVpValues: ProposalVpValues): Promise<number>
146184
const proposal = proposalVpValues.get(v.proposal)!;
147185
return {
148186
id: v.id,
187+
voter: v.voter,
188+
space: v.space,
149189
vpState: v.vpState,
150190
vpByStrategy: v.vpByStrategy,
151191
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/scores.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,15 @@ async function updateVotesVp(votes: any[], vpState: string, proposalId: string)
6262
votesInPage.forEach((vote: any) => {
6363
query += `UPDATE votes
6464
SET vp = ?, vp_by_strategy = ?, vp_state = ?, vp_value = ?, cb = ?
65-
WHERE id = ? AND proposal = ? LIMIT 1; `;
65+
WHERE id = ? AND proposal = ? AND cb != ? LIMIT 1; `;
6666
params.push(vote.balance);
6767
params.push(JSON.stringify(vote.scores));
6868
params.push(vpState);
6969
params.push(vote.vp_value);
7070
params.push(CB.PENDING_COMPUTE);
7171
params.push(vote.id);
7272
params.push(proposalId);
73+
params.push(CB.PENDING_DELETE);
7374
});
7475
await db.queryAsync(query, params);
7576
if (i) await snapshot.utils.sleep(200);

src/writer/alias.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,18 @@ export async function verify(message): Promise<any> {
1010
log.warn(`[writer] Wrong alias format ${JSON.stringify(schemaIsValid)}`);
1111
return Promise.reject('wrong alias format');
1212
}
13-
if (message.from === msg.payload.alias) {
13+
if (message.address === msg.payload.alias) {
1414
return Promise.reject('alias cannot be the same as the address');
1515
}
16+
17+
const [existing] = await db.queryAsync(
18+
'SELECT address FROM aliases WHERE alias = ? AND address != ? LIMIT 1',
19+
[msg.payload.alias, message.address]
20+
);
21+
if (existing) {
22+
return Promise.reject('alias is already linked to another address');
23+
}
24+
1625
return true;
1726
}
1827

@@ -25,5 +34,8 @@ export async function action(message, ipfs, receipt, id): Promise<void> {
2534
alias: msg.payload.alias,
2635
created: msg.timestamp
2736
};
28-
await db.queryAsync('INSERT INTO aliases SET ?', params);
37+
await db.queryAsync(
38+
'INSERT INTO aliases SET ? ON DUPLICATE KEY UPDATE id = VALUES(id), ipfs = VALUES(ipfs), created = VALUES(created)',
39+
params
40+
);
2941
}

0 commit comments

Comments
 (0)