Skip to content

feat: update proposal's value #570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 46 additions & 0 deletions src/helpers/entityValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
type Proposal = {
scores_by_strategy: number[][];
vp_value_by_strategy: number[];
};

/**
* 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(proposal: Proposal): number {
const { scores_by_strategy, vp_value_by_strategy } = proposal;

if (
!scores_by_strategy.length ||
!scores_by_strategy[0]?.length ||
!vp_value_by_strategy.length
) {
return 0;
}

let totalValue = 0;
for (let strategyIndex = 0; strategyIndex < vp_value_by_strategy.length; strategyIndex++) {
const strategyTotal = scores_by_strategy.reduce((sum, voteScores) => {
if (voteScores.length !== vp_value_by_strategy.length) {
throw new Error(
'Array size mismatch: voteScores length does not match vp_value_by_strategy length'
);
}
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 vp_value_by_strategy[strategyIndex] !== 'number') {
throw new Error(
`Invalid vp_value: expected number, got ${typeof vp_value_by_strategy[strategyIndex]}`
);
}

totalValue += strategyTotal * vp_value_by_strategy[strategyIndex];
}

return totalValue;
}
11 changes: 11 additions & 0 deletions 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 { getProposalValue } from './helpers/entityValue';
import log from './helpers/log';
import db from './helpers/mysql';
import { getDecryptionKey } from './helpers/shutter';
Expand All @@ -16,6 +17,7 @@ async function getProposal(id: string): Promise<any | undefined> {
proposal.choices = JSON.parse(proposal.choices);
proposal.scores = JSON.parse(proposal.scores);
proposal.scores_by_strategy = JSON.parse(proposal.scores_by_strategy);
proposal.vp_value_by_strategy = JSON.parse(proposal.vp_value_by_strategy);
let proposalState = 'pending';
const ts = parseInt((Date.now() / 1e3).toFixed());
if (ts > proposal.start) proposalState = 'active';
Expand Down Expand Up @@ -97,6 +99,12 @@ async function updateProposalScores(proposalId: string, scores: any, votes: numb
]);
}

async function updateProposalScoresValue(proposalId: string) {
const proposal = await getProposal(proposalId);
const query = 'UPDATE proposals SET scores_total_value = ? WHERE id = ? LIMIT 1;';
await db.queryAsync(query, [getProposalValue(proposal), proposalId]);
}

const pendingRequests = {};

export async function updateProposalAndVotes(proposalId: string, force = false) {
Expand Down Expand Up @@ -186,6 +194,9 @@ export async function updateProposalAndVotes(proposalId: string, force = false)
);

delete pendingRequests[proposalId];

await updateProposalScoresValue(proposalId);

return true;
} catch (e) {
delete pendingRequests[proposalId];
Expand Down
2 changes: 1 addition & 1 deletion test/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ CREATE TABLE proposals (
scores_state VARCHAR(24) NOT NULL DEFAULT '',
scores_total DECIMAL(64,30) NOT NULL,
scores_updated INT(11) NOT NULL,
scores_total_value DECIMAL(64,30) NOT NULL DEFAULT '0.000000000000000000000000000000',
scores_total_value DECIMAL(13,3) NOT NULL DEFAULT 0.000,
vp_value_by_strategy json NOT NULL,
votes INT(12) NOT NULL,
flagged INT NOT NULL DEFAULT 0,
Expand Down
157 changes: 157 additions & 0 deletions test/unit/helpers/entityValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { getProposalValue } from '../../../src/helpers/entityValue';

describe('getProposalValue', () => {
it('should calculate correct proposal value with single strategy', () => {
const proposal = {
scores_by_strategy: [[100], [200]],
vp_value_by_strategy: [2.5]
};

const result = getProposalValue(proposal);

expect(result).toBe(750); // (100 + 200) * 2.5 = 300 * 2.5 = 750
});

it('should calculate correct proposal value with multiple strategies', () => {
const proposal = {
scores_by_strategy: [
[100, 50],
[200, 75],
[300, 25]
],
vp_value_by_strategy: [1.5, 3.0]
};

const result = getProposalValue(proposal);

expect(result).toBe(1350); // (100+200+300)*1.5 + (50+75+25)*3.0 = 600*1.5 + 150*3.0 = 900 + 450 = 1350
});

it('should return 0 when scores_by_strategy is empty', () => {
const proposal = {
scores_by_strategy: [],
vp_value_by_strategy: [2.0]
};

const result = getProposalValue(proposal);

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

it('should return 0 when first strategy array is empty', () => {
const proposal = {
scores_by_strategy: [[]],
vp_value_by_strategy: [2.0]
};

const result = getProposalValue(proposal);

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

it('should return 0 when vp_value_by_strategy is empty', () => {
const proposal = {
scores_by_strategy: [[100], [200]],
vp_value_by_strategy: []
};

const result = getProposalValue(proposal);

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

it('should handle zero values correctly', () => {
const proposal = {
scores_by_strategy: [
[0, 0],
[0, 0]
],
vp_value_by_strategy: [2.0, 1.5]
};

const result = getProposalValue(proposal);

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

it('should handle zero vp_value_by_strategy correctly', () => {
const proposal = {
scores_by_strategy: [
[100, 50],
[200, 75]
],
vp_value_by_strategy: [0, 0]
};

const result = getProposalValue(proposal);

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

it('should handle decimal values correctly', () => {
const proposal = {
scores_by_strategy: [
[10.5, 20.5],
[15.5, 25.5]
],
vp_value_by_strategy: [0.1, 0.2]
};

const result = getProposalValue(proposal);

expect(result).toBe(11.8); // (10.5+15.5)*0.1 + (20.5+25.5)*0.2 = 26*0.1 + 46*0.2 = 2.6 + 9.2 = 11.8
});

it('should handle single vote scenario', () => {
const proposal = {
scores_by_strategy: [[100]],
vp_value_by_strategy: [2.0]
};

const result = getProposalValue(proposal);

expect(result).toBe(200); // 100 * 2.0 = 200
});

it('should throw on array size mismatch', () => {
const proposal = {
scores_by_strategy: [
[100, 50], // 2 strategies
[200, 75] // 2 strategies
],
vp_value_by_strategy: [1.5] // Only 1 strategy value
};

expect(() => getProposalValue(proposal)).toThrow(
'Array size mismatch: voteScores length does not match vp_value_by_strategy length'
);
});

it('should throw on invalid score value', () => {
const proposal = {
scores_by_strategy: [
[100, 'invalid'], // Invalid string value
[200, 75]
],
vp_value_by_strategy: [1.5, 2.0]
} as any;

expect(() => getProposalValue(proposal)).toThrow(
'Invalid score value: expected number, got string'
);
});

it('should throw on invalid vp_value', () => {
const proposal = {
scores_by_strategy: [
[100, 50],
[200, 75]
],
vp_value_by_strategy: [1.5, 'invalid'] // Invalid string value
} as any;

expect(() => getProposalValue(proposal)).toThrow(
'Invalid vp_value: expected number, got string'
);
});
});