diff --git a/.env.example b/.env.example index 3ff9dd04..d796819e 100644 --- a/.env.example +++ b/.env.example @@ -19,3 +19,4 @@ STARKNET_RPC_URL= # optional AUTH_SECRET=1dfd84a695705665668c260222ded178d1f1d62d251d7bee8148428dac6d0487 # optional WALLETCONNECT_PROJECT_ID=e6454bd61aba40b786e866a69bd4c5c6 REOWN_SECRET= # optional +LAST_CB=1 diff --git a/src/helpers/actions.ts b/src/helpers/actions.ts index 061c926d..913e8986 100644 --- a/src/helpers/actions.ts +++ b/src/helpers/actions.ts @@ -92,6 +92,7 @@ export async function getProposal(space, id) { params: {} }; proposal.choices = jsonParse(proposal.choices); + proposal.vp_value_by_strategy = jsonParse(proposal.vp_value_by_strategy, []); return proposal; } diff --git a/src/helpers/entityValue.ts b/src/helpers/entityValue.ts new file mode 100644 index 00000000..25e891b5 --- /dev/null +++ b/src/helpers/entityValue.ts @@ -0,0 +1,22 @@ +type Vote = { + vp_by_strategy: number[]; +}; + +type Proposal = { + vp_value_by_strategy: 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: Proposal, vote: Vote): number { + 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/writer/vote.ts b/src/writer/vote.ts index e86bac72..2d5b76bb 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -1,10 +1,13 @@ +import { capture } from '@snapshot-labs/snapshot-sentry'; import snapshot from '@snapshot-labs/snapshot.js'; import { getProposal } from '../helpers/actions'; +import { getVoteValue } from '../helpers/entityValue'; import log from '../helpers/log'; import db from '../helpers/mysql'; import { captureError, hasStrategyOverride, jsonParse } from '../helpers/utils'; import { updateProposalAndVotes } from '../scores'; +const LAST_CB = parseInt(process.env.LAST_CB ?? '1'); const scoreAPIUrl = process.env.SCORE_API_URL || 'https://score.snapshot.org'; // async function isLimitReached(space) { @@ -116,6 +119,18 @@ export async function action(body, ipfs, receipt, id, context): Promise { const withOverride = hasStrategyOverride(context.proposal.strategies); if (vpState === 'final' && withOverride) vpState = 'pending'; + // Get proposal voting power value + // Value is set on creation, and not updated on vote update + let vpValue = 0; + let cb = 0; + + try { + vpValue = getVoteValue(context.proposal, context.vp); + cb = LAST_CB; + } catch (e: any) { + capture(e, { msg, proposalId, context }); + } + const params = { id, ipfs, @@ -130,7 +145,8 @@ export async function action(body, ipfs, receipt, id, context): Promise { vp: context.vp.vp, vp_by_strategy: JSON.stringify(context.vp.vp_by_strategy), vp_state: vpState, - cb: 0 + vp_value: vpValue, + cb }; // Check if voter already voted diff --git a/test/schema.sql b/test/schema.sql index 59875ac3..7477fb10 100644 --- a/test/schema.sql +++ b/test/schema.sql @@ -100,6 +100,7 @@ CREATE TABLE votes ( vp DECIMAL(64,30) NOT NULL, vp_by_strategy JSON NOT NULL, vp_state VARCHAR(24) NOT NULL, + vp_value DECIMAL(13,3) NOT NULL DEFAULT 0.000, cb INT(11) NOT NULL, PRIMARY KEY (voter, space, proposal), UNIQUE KEY id (id), @@ -111,6 +112,7 @@ CREATE TABLE votes ( INDEX app (app), INDEX vp (vp), INDEX vp_state (vp_state), + INDEX vp_value (vp_value), INDEX cb (cb) ); diff --git a/test/unit/helpers/entityValue.test.ts b/test/unit/helpers/entityValue.test.ts new file mode 100644 index 00000000..7dba57b1 --- /dev/null +++ b/test/unit/helpers/entityValue.test.ts @@ -0,0 +1,71 @@ +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); + }); +});