Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
84efdb7
feat: set proposal vp value on proposal creation
wa0x6e Aug 3, 2025
f7a87bd
Merge branch 'master' into feat-get-proposal-vp-value-from-overlord
wa0x6e Aug 5, 2025
ed31f78
feat: set proposal strategies value on proposal creation
wa0x6e Aug 3, 2025
599f9e4
fix: set `cb` column
wa0x6e Aug 5, 2025
cb9859b
fix: default last_cb to 1
wa0x6e Aug 5, 2025
e589d82
fix: round strategies value up to 9 decimals
wa0x6e Aug 5, 2025
4fb62be
refactor: move precision to a const
wa0x6e Aug 5, 2025
43def63
chore: fix typo
wa0x6e Aug 5, 2025
4510db7
test: fix test
wa0x6e Aug 5, 2025
8a28e02
test: fix tests
wa0x6e Aug 5, 2025
8b8875e
refactor: use entityValue as value logic handler
wa0x6e Aug 6, 2025
afed3e3
fix: use randomUUID to generate random number
wa0x6e Aug 6, 2025
a2e025c
chore: use consistent logging
wa0x6e Aug 6, 2025
ec1028e
fix: handle overlord error
wa0x6e Aug 6, 2025
a64a0e6
fix: more precise rounding
wa0x6e Aug 6, 2025
adc6257
refactor: DRY json rpc request fetcher
wa0x6e Aug 7, 2025
602555e
refactor: simplify rounding
wa0x6e Aug 7, 2025
3e52e3a
fix: allow customize overlord url via env var
wa0x6e Aug 7, 2025
6589f97
chore: remove white spaces
wa0x6e Aug 7, 2025
d834326
Merge branch 'master' into feat-get-proposal-vp-value-from-overlord
wa0x6e Sep 16, 2025
585622a
fix: fix types
wa0x6e Sep 16, 2025
69d4ff8
chore: lint fix
wa0x6e Sep 16, 2025
e80d0c4
fix: update overlord url
wa0x6e Sep 16, 2025
83b9d1b
refactor: more consistent function name
wa0x6e Sep 16, 2025
c0761e1
fix: fix invalid method name
wa0x6e Sep 16, 2025
5ff71c4
fix: send JSON-RPC id as number
wa0x6e Sep 16, 2025
5b4ffd0
fix: skip vp value fetching for future proposals
wa0x6e Sep 16, 2025
7320439
Merge branch 'master' into feat-get-proposal-vp-value-from-overlord
wa0x6e Sep 17, 2025
f124efb
fix: remove the getValue setting on proposal creation
wa0x6e Sep 17, 2025
27d36b5
feat: refresh proposals vp_value_by_strategy async
wa0x6e Sep 17, 2025
933bb27
fix: skip votes vp value computation when proposal score value is not…
wa0x6e Sep 19, 2025
36cd966
fix: use only loop script to handle overlord logic
wa0x6e Sep 29, 2025
5a6534b
fix: process by most recent first
wa0x6e Sep 29, 2025
c70da7a
refactor: better function name
wa0x6e Sep 29, 2025
49a9749
fix: fix loop using same data
wa0x6e Sep 29, 2025
5725853
feat: add script to refresh votes' vp_value async
wa0x6e Sep 29, 2025
690c0a5
fix: on new votes using overriding strategies, mark all votes to be r…
wa0x6e Sep 29, 2025
cb5b124
Merge branch 'feat-get-proposal-vp-value-from-overlord' into feat-set…
wa0x6e Sep 29, 2025
486655d
fix: add new CB value to trigger other fields computation
wa0x6e Sep 29, 2025
44caefd
Merge branch 'feat-get-proposal-vp-value-from-overlord' into feat-set…
wa0x6e Sep 29, 2025
c94e6a8
fix: use dedicated CB when value need refresh
wa0x6e Sep 29, 2025
76ee137
fix: finalize votes CB
wa0x6e Sep 29, 2025
c79c4cf
Update src/helpers/proposalStrategiesValue.ts
wa0x6e Sep 30, 2025
e5595f3
fix: use static CB value for closed status
wa0x6e Sep 30, 2025
862cddf
refactoring: code improvement
wa0x6e Sep 30, 2025
2be6513
fix: better constant name
wa0x6e Sep 30, 2025
a19a334
Merge branch 'master' into feat-get-proposal-vp-value-from-overlord
wa0x6e Sep 30, 2025
3ca345c
fix: process older proposals first
wa0x6e Sep 30, 2025
5e2da2b
fix: send request in batch
wa0x6e Sep 30, 2025
b85c1ac
Merge branch 'feat-get-proposal-vp-value-from-overlord' into feat-set…
wa0x6e Sep 30, 2025
a23d2ee
fix: update to match proposal script convention
wa0x6e Sep 30, 2025
0004af2
Update src/helpers/votesVpValue.ts
wa0x6e Sep 30, 2025
b7f366a
chore: remove unused import
wa0x6e Sep 30, 2025
ef9498a
perf: better loop
wa0x6e Sep 30, 2025
7c0cc53
fix: mark votes as errored
wa0x6e Sep 30, 2025
2594734
feat: set proposal vp value on proposal creation
wa0x6e Aug 3, 2025
764e744
feat: set proposal strategies value on proposal creation
wa0x6e Aug 3, 2025
a4c07c4
fix: set `cb` column
wa0x6e Aug 5, 2025
18e7e5f
fix: default last_cb to 1
wa0x6e Aug 5, 2025
4ffbd36
fix: round strategies value up to 9 decimals
wa0x6e Aug 5, 2025
132912c
refactor: move precision to a const
wa0x6e Aug 5, 2025
0f0f641
chore: fix typo
wa0x6e Aug 5, 2025
a9b066f
test: fix test
wa0x6e Aug 5, 2025
bb35683
test: fix tests
wa0x6e Aug 5, 2025
1a63396
refactor: use entityValue as value logic handler
wa0x6e Aug 6, 2025
88a3b8c
fix: use randomUUID to generate random number
wa0x6e Aug 6, 2025
6a89832
chore: use consistent logging
wa0x6e Aug 6, 2025
b50edfc
fix: handle overlord error
wa0x6e Aug 6, 2025
b9163ab
fix: more precise rounding
wa0x6e Aug 6, 2025
5473075
refactor: DRY json rpc request fetcher
wa0x6e Aug 7, 2025
b98aa04
refactor: simplify rounding
wa0x6e Aug 7, 2025
4e55ece
fix: allow customize overlord url via env var
wa0x6e Aug 7, 2025
110893a
chore: remove white spaces
wa0x6e Aug 7, 2025
169e342
fix: fix types
wa0x6e Sep 16, 2025
0c01020
fix: update overlord url
wa0x6e Sep 16, 2025
fba0e95
refactor: more consistent function name
wa0x6e Sep 16, 2025
ee86a37
fix: fix invalid method name
wa0x6e Sep 16, 2025
f46a8a1
fix: send JSON-RPC id as number
wa0x6e Sep 16, 2025
57ef9b1
fix: skip vp value fetching for future proposals
wa0x6e Sep 16, 2025
f651c54
fix: remove the getValue setting on proposal creation
wa0x6e Sep 17, 2025
487d90d
feat: refresh proposals vp_value_by_strategy async
wa0x6e Sep 17, 2025
555bb9f
fix: skip votes vp value computation when proposal score value is not…
wa0x6e Sep 19, 2025
3bd4e96
fix: use only loop script to handle overlord logic
wa0x6e Sep 29, 2025
33ffe91
fix: process by most recent first
wa0x6e Sep 29, 2025
ce652ab
refactor: better function name
wa0x6e Sep 29, 2025
3f2a49e
feat: add script to refresh votes' vp_value async
wa0x6e Sep 29, 2025
f08703e
fix: on new votes using overriding strategies, mark all votes to be r…
wa0x6e Sep 29, 2025
d880145
fix: fix loop using same data
wa0x6e Sep 29, 2025
68e6f59
fix: add new CB value to trigger other fields computation
wa0x6e Sep 29, 2025
4f0e151
fix: use dedicated CB when value need refresh
wa0x6e Sep 29, 2025
96aff86
fix: finalize votes CB
wa0x6e Sep 29, 2025
7f8f3b9
Update src/helpers/proposalStrategiesValue.ts
wa0x6e Sep 30, 2025
93f6792
fix: use static CB value for closed status
wa0x6e Sep 30, 2025
7607541
refactoring: code improvement
wa0x6e Sep 30, 2025
610e24e
fix: better constant name
wa0x6e Sep 30, 2025
4059f3b
fix: process older proposals first
wa0x6e Sep 30, 2025
db63fcb
fix: send request in batch
wa0x6e Sep 30, 2025
7d9fda2
fix: update CB values
wa0x6e Sep 30, 2025
b2707a3
fix: update to match proposal script convention
wa0x6e Sep 30, 2025
53751bc
Update src/helpers/votesVpValue.ts
wa0x6e Sep 30, 2025
0849f02
Update src/helpers/proposalStrategiesValue.ts
wa0x6e Sep 30, 2025
51a3e6b
chore: remove unused import
wa0x6e Sep 30, 2025
81a1c97
feat: compute proposal's scores_total_value
wa0x6e Sep 30, 2025
0d900d3
fix: fix remnant from merge conflict
wa0x6e Sep 30, 2025
8fd5f32
Merge branch 'feat-set-vote-vp-value' into feat-set-proposal-scores-v…
wa0x6e Sep 30, 2025
c7b2d03
refactor: use camelCase for variable name
wa0x6e Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions src/helpers/entityValue.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { jsonRpcRequest } from './utils';

type Vote = {
vp_by_strategy: number[];
};

type Proposal = {
network: string;
strategies: any[];
Expand Down Expand Up @@ -33,15 +29,55 @@ export async function getVpValueByStrategy(proposal: Proposal): Promise<number[]
* 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;
export function getVoteValue(vp_value_by_strategy: number[], vp_by_strategy: number[]): number {
if (!vp_value_by_strategy.length) return 0;

if (proposal.vp_value_by_strategy.length !== vote.vp_by_strategy.length) {
if (vp_value_by_strategy.length !== 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
);
return vp_value_by_strategy.reduce((sum, value, index) => sum + value * vp_by_strategy[index], 0);
}

/**
* Calculates the proposal total value based on all votes' total voting power and the proposal's value per strategy.
* @returns The total value of the given proposal's votes, in the currency unit specified by the proposal's vp_value_by_strategy values
*/
export function getProposalValue(
scoresByStrategy: number[][],
vpValueByStrategy: number[]
): number {
if (!scoresByStrategy.length || !scoresByStrategy[0]?.length || !vpValueByStrategy.length) {
return 0;
}

// Validate that all voteScores arrays have the same length as vp_value_by_strategy
for (const voteScores of scoresByStrategy) {
if (voteScores.length !== vpValueByStrategy.length) {
throw new Error(
'Array size mismatch: voteScores length does not match vp_value_by_strategy length'
);
}
}

let totalValue = 0;
for (let strategyIndex = 0; strategyIndex < vpValueByStrategy.length; strategyIndex++) {
const strategyTotal = scoresByStrategy.reduce((sum, voteScores) => {
const score = voteScores[strategyIndex];
if (typeof score !== 'number') {
throw new Error(`Invalid score value: expected number, got ${typeof score}`);
}
return sum + score;
}, 0);

if (typeof vpValueByStrategy[strategyIndex] !== 'number') {
throw new Error(
`Invalid vp_value: expected number, got ${typeof vpValueByStrategy[strategyIndex]}`
);
}

totalValue += strategyTotal * vpValueByStrategy[strategyIndex];
}

return totalValue;
}
13 changes: 8 additions & 5 deletions src/helpers/proposalStrategiesValue.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// import { capture } from '@snapshot-labs/snapshot-sentry';
import snapshot from '@snapshot-labs/snapshot.js';
import { getVpValueByStrategy } from './entityValue';
import db from './mysql';
Expand Down Expand Up @@ -57,14 +58,16 @@ async function refreshVpByStrategy(proposals: Proposal[]) {

export default async function run() {
while (true) {
const proposals = await getProposals();
while (true) {
const proposals = await getProposals();

if (proposals.length === 0) break;

if (proposals.length) {
await refreshVpByStrategy(proposals);
}

if (proposals.length < BATCH_SIZE) {
await snapshot.utils.sleep(REFRESH_INTERVAL);
if (proposals.length < BATCH_SIZE) break;
}

await snapshot.utils.sleep(REFRESH_INTERVAL);
Comment on lines +61 to +71
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested infinite loops create unreachable code. The outer while loop will never continue after the inner loop breaks, making it effectively dead code.

Copilot uses AI. Check for mistakes.
}
}
69 changes: 69 additions & 0 deletions src/helpers/proposalsScoresValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import snapshot from '@snapshot-labs/snapshot.js';
import { getProposalValue } from './entityValue';
import db from './mysql';
import { CB } from '../constants';

type Proposal = {
id: string;
vpValueByStrategy: number[];
scoresByStrategy: number[][];
};

const REFRESH_INTERVAL = 10 * 1000;
const BATCH_SIZE = 25;

async function getProposals(): Promise<Proposal[]> {
const query = `
SELECT id, vp_value_by_strategy, scores_by_strategy
FROM proposals
WHERE cb = ? AND end < UNIX_TIMESTAMP() AND scores_state = ?
ORDER BY created ASC
LIMIT ?
`;
const proposals = await db.queryAsync(query, [CB.PENDING_CLOSE, 'final', BATCH_SIZE]);

return proposals.map((p: any) => {
p.scoresByStrategy = JSON.parse(p.vp_value_by_strategy);
p.vpValueByStrategy = JSON.parse(p.scores_by_strategy);
return p;
});
}

async function refreshScoresTotal(proposals: Proposal[]) {
const query: string[] = [];
const params: any[] = [];

proposals.map(proposal => {
query.push('UPDATE proposals SET scores_total_value = ?, cb = ? WHERE id = ? LIMIT 1');

try {
const scoresTotalValue = getProposalValue(
proposal.scoresByStrategy,
proposal.vpValueByStrategy
);
params.push(scoresTotalValue, CB.FINAL, proposal.id);
} catch (e) {
capture(e);
params.push(0, CB.INELIGIBLE, proposal.id);
}
});

if (query.length) {
await db.queryAsync(query.join(';'), params);
}
}

export default async function run() {
while (true) {
const proposals = await getProposals();

if (proposals.length) {
await refreshScoresTotal(proposals);
}

if (proposals.length < BATCH_SIZE) {
await snapshot.utils.sleep(REFRESH_INTERVAL);
}
}
}
71 changes: 71 additions & 0 deletions src/helpers/votesVpValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import snapshot from '@snapshot-labs/snapshot.js';
import { getVoteValue } from './entityValue';
import db from './mysql';
import { CB } from '../constants';

const REFRESH_INTERVAL = 10 * 1000;
const BATCH_SIZE = 100;

type Datum = {
id: string;
vp_state: string;
vp_by_strategy: number[];
vp_value_by_strategy: number[];
};

async function getVotes(): Promise<Datum[]> {
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 IN (?)
ORDER BY votes.created ASC
LIMIT ?`;
const results = await db.queryAsync(query, [
[CB.PENDING_CLOSE, CB.PENDING_COMPUTE, CB.FINAL],
[CB.PENDING_SYNC, CB.PENDING_COMPUTE],
BATCH_SIZE
]);

return results.map((r: any) => {
r.vp_value_by_strategy = JSON.parse(r.vp_value_by_strategy);
r.vp_by_strategy = JSON.parse(r.vp_by_strategy);
return r;
});
}

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 value = getVoteValue(datum.vp_value_by_strategy, datum.vp_by_strategy);

params.push(value, datum.vp_state === 'final' ? CB.FINAL : CB.PENDING_CLOSE, datum.id);
} catch (e) {
console.log(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);
}
}
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import api from './api';
import log from './helpers/log';
import initMetrics from './helpers/metrics';
import refreshModeration from './helpers/moderation';
import refreshProposalsScoresValue from './helpers/proposalsScoresValue';
import refreshProposalsVpValue from './helpers/proposalStrategiesValue';
import rateLimit from './helpers/rateLimit';
import shutter from './helpers/shutter';
Expand All @@ -15,13 +16,16 @@ import {
stop as stopStrategies
} from './helpers/strategies';
import { trackTurboStatuses } from './helpers/turbo';
import refreshVotesVpValue from './helpers/votesVpValue';

const app = express();

async function startServer() {
initLogger(app);
refreshModeration();
refreshProposalsVpValue();
refreshVotesVpValue();
refreshProposalsScoresValue();

await initializeStrategies();
refreshStrategies();
Expand Down
4 changes: 3 additions & 1 deletion src/scores.ts
Original file line number Diff line number Diff line change
@@ -1,4 +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';
Expand Down Expand Up @@ -61,12 +62,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);
});
Expand Down
61 changes: 37 additions & 24 deletions test/unit/helpers/entityValue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,82 @@ 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 vp_value_by_strategy = [2.5];
const vp_by_strategy = [100];

const result = getVoteValue(proposal, vote);
const result = getVoteValue(vp_value_by_strategy, vp_by_strategy);

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 vp_value_by_strategy = [1.5, 3.0, 0.5];
const vp_by_strategy = [100, 50, 200];

const result = getVoteValue(proposal, vote);
const result = getVoteValue(vp_value_by_strategy, vp_by_strategy);

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 vp_value_by_strategy = [2.0, 1.5];
const vp_by_strategy = [0, 0];

const result = getVoteValue(proposal, vote);
const result = getVoteValue(vp_value_by_strategy, vp_by_strategy);

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 vp_value_by_strategy = [0, 0];
const vp_by_strategy = [100, 50];

const result = getVoteValue(proposal, vote);
const result = getVoteValue(vp_value_by_strategy, vp_by_strategy);

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 vp_value_by_strategy = [0.1, 0.25];
const vp_by_strategy = [10, 20];

const result = getVoteValue(proposal, vote);
const result = getVoteValue(vp_value_by_strategy, vp_by_strategy);

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] };
const vp_value_by_strategy = [1.0, 2.0];
const vp_by_strategy = [100];

expect(() => getVoteValue(proposal, vote)).toThrow('invalid data to compute vote value');
expect(() => getVoteValue(vp_value_by_strategy, vp_by_strategy)).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] };
const vp_value_by_strategy = [1.0];
const vp_by_strategy = [100, 50];

expect(() => getVoteValue(proposal, vote)).toThrow('invalid data to compute vote value');
expect(() => getVoteValue(vp_value_by_strategy, vp_by_strategy)).toThrow(
'invalid data to compute vote value'
);
});

it('should handle empty arrays', () => {
const proposal = { vp_value_by_strategy: [] };
const vote = { vp_by_strategy: [] };
const vp_value_by_strategy = [];
const vp_by_strategy = [];

const result = getVoteValue(proposal, vote);
const result = getVoteValue(vp_value_by_strategy, vp_by_strategy);

expect(result).toBe(0);
});

it('should return 0 when vp_value_by_strategy is empty', () => {
const vp_value_by_strategy = [];
const vp_by_strategy = [100, 50];

const result = getVoteValue(vp_value_by_strategy, vp_by_strategy);

expect(result).toBe(0);
});
Expand Down
Loading
Loading