From 4383ded2135e4e7c8a27d3a36938dbb8662b8cca Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Thu, 31 Jul 2025 02:55:12 +0400 Subject: [PATCH 01/15] feat: compute vote fiat value feat: add index to vp_value fix: handle high precision computation perf: fix: handle error fix: increase last vote cb --- src/helpers/actions.ts | 1 + src/helpers/utils.ts | 113 ++++++++++++++++++++++++++++++ src/writer/vote.ts | 18 ++++- test/schema.sql | 2 + test/unit/helpers/utils.test.ts | 118 ++++++++++++++++++++++++++++++++ 5 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 test/unit/helpers/utils.test.ts 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/utils.ts b/src/helpers/utils.ts index 95de6c99..7c65a3f8 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -266,3 +266,116 @@ export function getSpaceController(space: string, network = NETWORK) { return snapshot.utils.getSpaceController(space, networkId, { broviderUrl }); } + +/** + * Recursively checks if two arrays have the exact same structure, + * including nesting levels at corresponding positions, and contain only valid numbers. + * + * @param arrayA - First array + * @param arrayB - Second array + * @returns True if structures match exactly and contain only numbers, false otherwise + */ +function arraysHaveSameStructure(arrayA: any, arrayB: any): boolean { + // Both must be arrays or both must not be arrays + if (Array.isArray(arrayA) !== Array.isArray(arrayB)) { + return false; + } + + // If neither is an array, check they are valid numeric values + if (!Array.isArray(arrayA)) { + return ( + typeof arrayA === 'number' && !isNaN(arrayA) && typeof arrayB === 'number' && !isNaN(arrayB) + ); + } + + // Both are arrays - check they have the same length + if (arrayA.length !== arrayB.length) { + return false; + } + + // Recursively check each element has the same structure and valid content + for (let i = 0; i < arrayA.length; i++) { + if (!arraysHaveSameStructure(arrayA[i], arrayB[i])) { + return false; + } + } + + return true; +} + +/** + * Validates and prepares arrays for dot product calculation. + * Ensures arrays have identical structure and nesting patterns. + * + * @param arrayA - First array + * @param arrayB - Second array + * @returns Object with flattened arrays, or null if invalid or structure mismatch + */ +function validateAndFlattenArrays( + arrayA: any, + arrayB: any +): { flatArrayA: any[]; flatArrayB: any[] } | null { + if (!Array.isArray(arrayA) || !Array.isArray(arrayB)) { + return null; + } + + // Check for exact structural match + if (!arraysHaveSameStructure(arrayA, arrayB)) { + return null; + } + + // Structure matches, safe to flatten + const flatArrayA = arrayA.flat(Infinity); + const flatArrayB = arrayB.flat(Infinity); + + return { flatArrayA, flatArrayB }; +} + +/** + * Computes the dot product of two arrays by multiplying corresponding elements and summing the results. + * + * This function performs a dot product calculation between two arrays of numbers using + * JavaScript's native arithmetic. Both arrays are flattened to handle nested structures + * of unlimited depth before calculation. Arrays must have identical structure and contain only numbers. + * + * @param arrayA - First array of numbers. Can contain deeply nested arrays of any depth + * @param arrayB - Second array of numbers. Must have the same structure as arrayA after flattening + * @returns The computed dot product as a number. Returns 0 if arrays are invalid or mismatched. + * + * @example + * // Simple arrays + * dotProduct([1, 2, 3], [10, 20, 30]) // Returns 140 (1*10 + 2*20 + 3*30) + * + * @example + * // Nested arrays (2 levels) + * dotProduct([1, [2, 3]], [10, [20, 30]]) // Returns 140 (1*10 + 2*20 + 3*30) + * + * @example + * // Financial calculations + * dotProduct([1.833444691890596], [1000.123456789]) // Uses JavaScript precision + */ +export function dotProduct(arrayA: any[], arrayB: any[]): number { + const validation = validateAndFlattenArrays(arrayA, arrayB); + if (!validation) { + throw new Error('Invalid arrays structure mismatch'); + } + + const { flatArrayA, flatArrayB } = validation; + + // Use pure JavaScript arithmetic for all calculations + let sum = 0; + + for (let i = 0; i < flatArrayA.length; i++) { + const numA = flatArrayA[i]; + const numB = flatArrayB[i]; + + const product = numA * numB; + + // Only add finite numbers to avoid NaN propagation + if (isFinite(product)) { + sum += product; + } + } + + return sum; +} diff --git a/src/writer/vote.ts b/src/writer/vote.ts index e86bac72..b66f2d8b 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -1,10 +1,12 @@ +import { capture } from '@snapshot-labs/snapshot-sentry'; import snapshot from '@snapshot-labs/snapshot.js'; import { getProposal } from '../helpers/actions'; import log from '../helpers/log'; import db from '../helpers/mysql'; -import { captureError, hasStrategyOverride, jsonParse } from '../helpers/utils'; +import { captureError, dotProduct, hasStrategyOverride, jsonParse } from '../helpers/utils'; import { updateProposalAndVotes } from '../scores'; +const LAST_VOTE_CB = 1; const scoreAPIUrl = process.env.SCORE_API_URL || 'https://score.snapshot.org'; // async function isLimitReached(space) { @@ -116,6 +118,17 @@ export async function action(body, ipfs, receipt, id, context): Promise { const withOverride = hasStrategyOverride(context.proposal.strategies); if (vpState === 'final' && withOverride) vpState = 'pending'; + // Compute vote value + let vp_value = 0; + let cb = 0; + + try { + vp_value = dotProduct(context.proposal.vp_value_by_strategy, context.vp.vp_by_strategy); + cb = LAST_VOTE_CB; + } catch (e: any) { + capture(e, { msg, proposalId }); + } + const params = { id, ipfs, @@ -130,7 +143,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, + 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/utils.test.ts b/test/unit/helpers/utils.test.ts new file mode 100644 index 00000000..b59c4032 --- /dev/null +++ b/test/unit/helpers/utils.test.ts @@ -0,0 +1,118 @@ +import { dotProduct } from '../../../src/helpers/utils'; + +describe('utils', () => { + describe('dotProduct()', () => { + describe('Input Validation', () => { + it('should return 0 for invalid array inputs', () => { + expect(dotProduct(null as any, [1, 2])).toBe(0); + expect(dotProduct([1, 2], null as any)).toBe(0); + expect(dotProduct(undefined as any, [1, 2])).toBe(0); + expect(dotProduct([1, 2], undefined as any)).toBe(0); + expect(dotProduct('string' as any, [1, 2])).toBe(0); + expect(dotProduct([1, 2], 'string' as any)).toBe(0); + }); + + it('should return 0 for arrays of different lengths', () => { + expect(dotProduct([1, 2], [3])).toBe(0); + expect(dotProduct([1], [2, 3, 4])).toBe(0); + expect(dotProduct([], [1, 2])).toBe(0); // Empty vs non-empty + expect(dotProduct([], [])).toBe(0); // Both empty + }); + + it('should reject arrays with non-numeric values', () => { + expect(dotProduct([1, null, 3], [4, 5, 6])).toBe(0); // Should reject null values + expect(dotProduct([1, undefined, 3], [4, 5, 6])).toBe(0); // Should reject undefined values + expect(dotProduct([1, 2, 3], [4, null, 6])).toBe(0); // Should reject null values + expect(dotProduct(['1', '2'], ['3', '4'])).toBe(0); // Should reject string numbers + expect(dotProduct([1, '2'], [3, '4'])).toBe(0); // Should reject mixed types + expect(dotProduct([1, 'invalid', 3], [4, 5, 6])).toBe(0); // Should reject invalid strings + expect(dotProduct([1, {}, 3], [4, 5, 6])).toBe(0); // Should reject objects + expect(dotProduct([1, [], 3], [4, 5, 6])).toBe(0); // Should reject arrays as values + }); + + it('should reject mixed flat and nested arrays with different structures', () => { + expect(dotProduct([1, 2], [[3], 4])).toBe(0); // Different structures - should reject + expect(dotProduct([[1], 2], [3, 4])).toBe(0); // Different structures - should reject + }); + }); + + describe('Basic Calculations', () => { + it('should calculate dot product for simple arrays', () => { + expect(dotProduct([1, 2, 3], [4, 5, 6])).toBe(32); // 1*4 + 2*5 + 3*6 = 32 + expect(dotProduct([2, 3], [4, 5])).toBe(23); // 2*4 + 3*5 = 23 + expect(dotProduct([1], [5])).toBe(5); // 1*5 = 5 + }); + + it('should handle arrays with zeros', () => { + expect(dotProduct([1, 0, 3], [4, 5, 6])).toBe(22); // 1*4 + 0*5 + 3*6 = 22 + expect(dotProduct([0, 0, 0], [1, 2, 3])).toBe(0); // All zeros in first array + }); + + it('should handle negative numbers', () => { + expect(dotProduct([-1, 2], [3, -4])).toBe(-11); // -1*3 + 2*(-4) = -11 + expect(dotProduct([-2, -3], [-4, -5])).toBe(23); // -2*(-4) + -3*(-5) = 23 + }); + }); + + describe('Nested Array Support', () => { + it('should handle nested arrays (single level)', () => { + expect(dotProduct([1, [2, 3]], [4, [5, 6]])).toBe(32); // Flattened: [1,2,3] • [4,5,6] = 32 + }); + + it('should handle deeply nested arrays', () => { + expect(dotProduct([1, [2, [3, 4]]], [5, [6, [7, 8]]])).toBe(70); // [1,2,3,4] • [5,6,7,8] = 70 + expect(dotProduct([[[1]], [2]], [[[3]], [4]])).toBe(11); // [1,2] • [3,4] = 11 + }); + }); + + describe('JavaScript Native Precision', () => { + it('should handle large × large number multiplication', () => { + // Test realistic large financial numbers + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + const votingPower = [123456789012.456789]; + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + const tokenValue = [987654321.123456789]; + + const result = dotProduct(votingPower, tokenValue); + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + const expected = 123456789012.456789 * 987654321.123456789; + + expect(result).toBe(expected); + }); + + it('should handle small × large number multiplication (DeFi scenario)', () => { + // Test small token values with large voting power + const smallTokenValues = [1e-18, 1e-12, 1e-6]; // Wei, micro, milli units + const largeVotingPower = [1e18, 1e15, 1e12]; // Large voting power values + + const result = dotProduct(smallTokenValues, largeVotingPower); + + // Should equal: 1 + 1000 + 1000000 = 1001001 + expect(result).toBe(1001001); + }); + + it('should handle maximum precision decimal numbers', () => { + // Test JavaScript's precision limits (~15-16 significant digits) + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + const maxDecimalA = [1.1234567890123456]; + const maxDecimalB = [2.9876543210987654]; + + const result = dotProduct(maxDecimalA, maxDecimalB); + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + const expected = 1.1234567890123456 * 2.9876543210987654; + + expect(result).toBe(expected); + }); + + it('should handle underflow edge cases', () => { + // Test numbers that underflow to 0 + const verySmallA = [1e-200]; + const verySmallB = [1e-200]; + + const result = dotProduct(verySmallA, verySmallB); + + expect(result).toBe(0); // JavaScript underflow behavior + }); + }); + }); +}); From 4715de875751a23adba79e8af44afd0bbf062c35 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Tue, 5 Aug 2025 21:54:16 +0400 Subject: [PATCH 02/15] test: fix test for new thrown error --- test/unit/helpers/utils.test.ts | 69 +++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/test/unit/helpers/utils.test.ts b/test/unit/helpers/utils.test.ts index b59c4032..0458661b 100644 --- a/test/unit/helpers/utils.test.ts +++ b/test/unit/helpers/utils.test.ts @@ -3,36 +3,57 @@ import { dotProduct } from '../../../src/helpers/utils'; describe('utils', () => { describe('dotProduct()', () => { describe('Input Validation', () => { - it('should return 0 for invalid array inputs', () => { - expect(dotProduct(null as any, [1, 2])).toBe(0); - expect(dotProduct([1, 2], null as any)).toBe(0); - expect(dotProduct(undefined as any, [1, 2])).toBe(0); - expect(dotProduct([1, 2], undefined as any)).toBe(0); - expect(dotProduct('string' as any, [1, 2])).toBe(0); - expect(dotProduct([1, 2], 'string' as any)).toBe(0); + it('should throw error for invalid array inputs', () => { + expect(() => dotProduct(null as any, [1, 2])).toThrow('Invalid arrays structure mismatch'); + expect(() => dotProduct([1, 2], null as any)).toThrow('Invalid arrays structure mismatch'); + expect(() => dotProduct(undefined as any, [1, 2])).toThrow( + 'Invalid arrays structure mismatch' + ); + expect(() => dotProduct([1, 2], undefined as any)).toThrow( + 'Invalid arrays structure mismatch' + ); + expect(() => dotProduct('string' as any, [1, 2])).toThrow( + 'Invalid arrays structure mismatch' + ); + expect(() => dotProduct([1, 2], 'string' as any)).toThrow( + 'Invalid arrays structure mismatch' + ); }); - it('should return 0 for arrays of different lengths', () => { - expect(dotProduct([1, 2], [3])).toBe(0); - expect(dotProduct([1], [2, 3, 4])).toBe(0); - expect(dotProduct([], [1, 2])).toBe(0); // Empty vs non-empty - expect(dotProduct([], [])).toBe(0); // Both empty + it('should throw error for arrays of different lengths', () => { + expect(() => dotProduct([1, 2], [3])).toThrow('Invalid arrays structure mismatch'); + expect(() => dotProduct([1], [2, 3, 4])).toThrow('Invalid arrays structure mismatch'); + expect(() => dotProduct([], [1, 2])).toThrow('Invalid arrays structure mismatch'); // Empty vs non-empty }); - it('should reject arrays with non-numeric values', () => { - expect(dotProduct([1, null, 3], [4, 5, 6])).toBe(0); // Should reject null values - expect(dotProduct([1, undefined, 3], [4, 5, 6])).toBe(0); // Should reject undefined values - expect(dotProduct([1, 2, 3], [4, null, 6])).toBe(0); // Should reject null values - expect(dotProduct(['1', '2'], ['3', '4'])).toBe(0); // Should reject string numbers - expect(dotProduct([1, '2'], [3, '4'])).toBe(0); // Should reject mixed types - expect(dotProduct([1, 'invalid', 3], [4, 5, 6])).toBe(0); // Should reject invalid strings - expect(dotProduct([1, {}, 3], [4, 5, 6])).toBe(0); // Should reject objects - expect(dotProduct([1, [], 3], [4, 5, 6])).toBe(0); // Should reject arrays as values + it('should throw error for arrays with non-numeric values', () => { + expect(() => dotProduct([1, null, 3], [4, 5, 6])).toThrow( + 'Invalid arrays structure mismatch' + ); // Should reject null values + expect(() => dotProduct([1, undefined, 3], [4, 5, 6])).toThrow( + 'Invalid arrays structure mismatch' + ); // Should reject undefined values + expect(() => dotProduct([1, 2, 3], [4, null, 6])).toThrow( + 'Invalid arrays structure mismatch' + ); // Should reject null values + expect(() => dotProduct(['1', '2'], ['3', '4'])).toThrow( + 'Invalid arrays structure mismatch' + ); // Should reject string numbers + expect(() => dotProduct([1, '2'], [3, '4'])).toThrow('Invalid arrays structure mismatch'); // Should reject mixed types + expect(() => dotProduct([1, 'invalid', 3], [4, 5, 6])).toThrow( + 'Invalid arrays structure mismatch' + ); // Should reject invalid strings + expect(() => dotProduct([1, {}, 3], [4, 5, 6])).toThrow( + 'Invalid arrays structure mismatch' + ); // Should reject objects + expect(() => dotProduct([1, [], 3], [4, 5, 6])).toThrow( + 'Invalid arrays structure mismatch' + ); // Should reject arrays as values }); - it('should reject mixed flat and nested arrays with different structures', () => { - expect(dotProduct([1, 2], [[3], 4])).toBe(0); // Different structures - should reject - expect(dotProduct([[1], 2], [3, 4])).toBe(0); // Different structures - should reject + it('should throw error for mixed flat and nested arrays with different structures', () => { + expect(() => dotProduct([1, 2], [[3], 4])).toThrow('Invalid arrays structure mismatch'); // Different structures - should reject + expect(() => dotProduct([[1], 2], [3, 4])).toThrow('Invalid arrays structure mismatch'); // Different structures - should reject }); }); From fcfb9dff2bfef8a5b2e7814f251a4999b3c0398c Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 6 Aug 2025 00:39:30 +0400 Subject: [PATCH 03/15] fix: get last_cb from env var --- .env.example | 1 + src/writer/vote.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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/writer/vote.ts b/src/writer/vote.ts index b66f2d8b..3d0c52b2 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -6,7 +6,7 @@ import db from '../helpers/mysql'; import { captureError, dotProduct, hasStrategyOverride, jsonParse } from '../helpers/utils'; import { updateProposalAndVotes } from '../scores'; -const LAST_VOTE_CB = 1; +const LAST_CB = parseInt(process.env.LAST_CB ?? '1'); const scoreAPIUrl = process.env.SCORE_API_URL || 'https://score.snapshot.org'; // async function isLimitReached(space) { @@ -124,7 +124,7 @@ export async function action(body, ipfs, receipt, id, context): Promise { try { vp_value = dotProduct(context.proposal.vp_value_by_strategy, context.vp.vp_by_strategy); - cb = LAST_VOTE_CB; + cb = LAST_CB; } catch (e: any) { capture(e, { msg, proposalId }); } From 433f4fea36586a5ff8f59d007d8783b2014864ca Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 6 Aug 2025 02:46:44 +0400 Subject: [PATCH 04/15] refactor: more generic array computation function --- src/helpers/utils.ts | 162 +++++++++++++------------------- src/writer/vote.ts | 9 +- test/unit/helpers/utils.test.ts | 161 ++++++++++++++++++++++--------- 3 files changed, 191 insertions(+), 141 deletions(-) diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 6bc7efb3..6d537082 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -269,114 +269,86 @@ export function getSpaceController(space: string, network = NETWORK) { } /** - * Recursively checks if two arrays have the exact same structure, - * including nesting levels at corresponding positions, and contain only valid numbers. + * Performs element-wise operations on two arrays while preserving their structure. * - * @param arrayA - First array - * @param arrayB - Second array - * @returns True if structures match exactly and contain only numbers, false otherwise - */ -function arraysHaveSameStructure(arrayA: any, arrayB: any): boolean { - // Both must be arrays or both must not be arrays - if (Array.isArray(arrayA) !== Array.isArray(arrayB)) { - return false; - } - - // If neither is an array, check they are valid numeric values - if (!Array.isArray(arrayA)) { - return ( - typeof arrayA === 'number' && !isNaN(arrayA) && typeof arrayB === 'number' && !isNaN(arrayB) - ); - } - - // Both are arrays - check they have the same length - if (arrayA.length !== arrayB.length) { - return false; - } - - // Recursively check each element has the same structure and valid content - for (let i = 0; i < arrayA.length; i++) { - if (!arraysHaveSameStructure(arrayA[i], arrayB[i])) { - return false; - } - } - - return true; -} - -/** - * Validates and prepares arrays for dot product calculation. - * Ensures arrays have identical structure and nesting patterns. + * This function applies the specified operation between corresponding elements of two arrays. + * The result maintains the same nested structure as the input arrays. + * Arrays must have identical structure and contain only numbers. * - * @param arrayA - First array - * @param arrayB - Second array - * @returns Object with flattened arrays, or null if invalid or structure mismatch - */ -function validateAndFlattenArrays( - arrayA: any, - arrayB: any -): { flatArrayA: any[]; flatArrayB: any[] } | null { - if (!Array.isArray(arrayA) || !Array.isArray(arrayB)) { - return null; - } - - // Check for exact structural match - if (!arraysHaveSameStructure(arrayA, arrayB)) { - return null; - } - - // Structure matches, safe to flatten - const flatArrayA = arrayA.flat(Infinity); - const flatArrayB = arrayB.flat(Infinity); - - return { flatArrayA, flatArrayB }; -} - -/** - * Computes the dot product of two arrays by multiplying corresponding elements and summing the results. + * @param arrayA - First array of numbers. Can contain deeply nested arrays of any depth + * @param arrayB - Second array of numbers. Must have the same structure as arrayA + * @param operation - Operation to perform on corresponding elements: + * - 'multiply': a * b (default) + * - 'add': a + b + * - 'subtract': a - b + * - 'divide': a / b (returns 0 when dividing by 0) + * @returns Array with same structure as inputs, containing results from element-wise operations + * @throws {Error} Throws 'Invalid arrays structure mismatch' if arrays have different structures or contain non-numeric values * - * This function performs a dot product calculation between two arrays of numbers using - * JavaScript's native arithmetic. Both arrays are flattened to handle nested structures - * of unlimited depth before calculation. Arrays must have identical structure and contain only numbers. + * @example + * // Element-wise multiplication (default) + * arrayOperation([1, 2, 3], [4, 5, 6]) // Returns [4, 10, 18] * - * @param arrayA - First array of numbers. Can contain deeply nested arrays of any depth - * @param arrayB - Second array of numbers. Must have the same structure as arrayA after flattening - * @returns The computed dot product as a number. Returns 0 if arrays are invalid or mismatched. + * @example + * // Element-wise addition + * arrayOperation([1, 2, 3], [10, 20, 30], 'add') // Returns [11, 22, 33] * * @example - * // Simple arrays - * dotProduct([1, 2, 3], [10, 20, 30]) // Returns 140 (1*10 + 2*20 + 3*30) + * // Element-wise subtraction + * arrayOperation([10, 20, 30], [1, 2, 3], 'subtract') // Returns [9, 18, 27] * * @example - * // Nested arrays (2 levels) - * dotProduct([1, [2, 3]], [10, [20, 30]]) // Returns 140 (1*10 + 2*20 + 3*30) + * // Element-wise division + * arrayOperation([10, 20, 30], [2, 4, 5], 'divide') // Returns [5, 5, 6] * * @example - * // Financial calculations - * dotProduct([1.833444691890596], [1000.123456789]) // Uses JavaScript precision + * // Nested arrays (structure preserved) + * arrayOperation([1, [2, 3]], [10, [20, 30]], 'multiply') // Returns [10, [40, 90]] */ -export function dotProduct(arrayA: any[], arrayB: any[]): number { - const validation = validateAndFlattenArrays(arrayA, arrayB); - if (!validation) { - throw new Error('Invalid arrays structure mismatch'); - } - - const { flatArrayA, flatArrayB } = validation; - - // Use pure JavaScript arithmetic for all calculations - let sum = 0; - - for (let i = 0; i < flatArrayA.length; i++) { - const numA = flatArrayA[i]; - const numB = flatArrayB[i]; - - const product = numA * numB; +export function arrayOperation( + arrayA: any[], + arrayB: any[], + operation: 'multiply' | 'add' | 'subtract' | 'divide' = 'multiply' +): any[] { + // Recursive function to process arrays while maintaining structure + function processArrays(a: any, b: any): any { + // If both are not arrays, they should be numbers + if (!Array.isArray(a) && !Array.isArray(b) && typeof a === 'number' && typeof b === 'number') { + let result: number; + switch (operation) { + case 'multiply': + result = a * b; + break; + case 'add': + result = a + b; + break; + case 'subtract': + result = a - b; + break; + case 'divide': + result = a / b; + break; + default: + result = a * b; + } + // Throw error if result is not finite + if (!isFinite(result)) { + throw new Error('Operation resulted in infinity or NaN'); + } + return result; + } - // Only add finite numbers to avoid NaN propagation - if (isFinite(product)) { - sum += product; + // If both are arrays, process each element recursively + if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) { + const results: any[] = []; + for (let i = 0; i < a.length; i++) { + results.push(processArrays(a[i], b[i])); + } + return results; } + + throw new Error('Invalid arrays structure mismatch'); } - return sum; + return processArrays(arrayA, arrayB); } diff --git a/src/writer/vote.ts b/src/writer/vote.ts index 3d0c52b2..1838d08d 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -3,7 +3,7 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { getProposal } from '../helpers/actions'; import log from '../helpers/log'; import db from '../helpers/mysql'; -import { captureError, dotProduct, hasStrategyOverride, jsonParse } from '../helpers/utils'; +import { arrayOperation, captureError, hasStrategyOverride, jsonParse } from '../helpers/utils'; import { updateProposalAndVotes } from '../scores'; const LAST_CB = parseInt(process.env.LAST_CB ?? '1'); @@ -123,7 +123,12 @@ export async function action(body, ipfs, receipt, id, context): Promise { let cb = 0; try { - vp_value = dotProduct(context.proposal.vp_value_by_strategy, context.vp.vp_by_strategy); + const products = arrayOperation( + context.proposal.vp_value_by_strategy, + context.vp.vp_by_strategy, + 'multiply' + ); + vp_value = products.flat(Infinity).reduce((sum, val) => sum + val, 0); cb = LAST_CB; } catch (e: any) { capture(e, { msg, proposalId }); diff --git a/test/unit/helpers/utils.test.ts b/test/unit/helpers/utils.test.ts index 0458661b..73aed5f9 100644 --- a/test/unit/helpers/utils.test.ts +++ b/test/unit/helpers/utils.test.ts @@ -1,88 +1,120 @@ -import { dotProduct } from '../../../src/helpers/utils'; +import { arrayOperation } from '../../../src/helpers/utils'; describe('utils', () => { - describe('dotProduct()', () => { + describe('arrayOperation()', () => { describe('Input Validation', () => { it('should throw error for invalid array inputs', () => { - expect(() => dotProduct(null as any, [1, 2])).toThrow('Invalid arrays structure mismatch'); - expect(() => dotProduct([1, 2], null as any)).toThrow('Invalid arrays structure mismatch'); - expect(() => dotProduct(undefined as any, [1, 2])).toThrow( + expect(() => arrayOperation(null as any, [1, 2])).toThrow( 'Invalid arrays structure mismatch' ); - expect(() => dotProduct([1, 2], undefined as any)).toThrow( + expect(() => arrayOperation([1, 2], null as any)).toThrow( 'Invalid arrays structure mismatch' ); - expect(() => dotProduct('string' as any, [1, 2])).toThrow( + expect(() => arrayOperation(undefined as any, [1, 2])).toThrow( 'Invalid arrays structure mismatch' ); - expect(() => dotProduct([1, 2], 'string' as any)).toThrow( + expect(() => arrayOperation([1, 2], undefined as any)).toThrow( + 'Invalid arrays structure mismatch' + ); + expect(() => arrayOperation('string' as any, [1, 2])).toThrow( + 'Invalid arrays structure mismatch' + ); + expect(() => arrayOperation([1, 2], 'string' as any)).toThrow( 'Invalid arrays structure mismatch' ); }); it('should throw error for arrays of different lengths', () => { - expect(() => dotProduct([1, 2], [3])).toThrow('Invalid arrays structure mismatch'); - expect(() => dotProduct([1], [2, 3, 4])).toThrow('Invalid arrays structure mismatch'); - expect(() => dotProduct([], [1, 2])).toThrow('Invalid arrays structure mismatch'); // Empty vs non-empty + expect(() => arrayOperation([1, 2], [3])).toThrow('Invalid arrays structure mismatch'); + expect(() => arrayOperation([1], [2, 3, 4])).toThrow('Invalid arrays structure mismatch'); + expect(() => arrayOperation([], [1, 2])).toThrow('Invalid arrays structure mismatch'); // Empty vs non-empty }); it('should throw error for arrays with non-numeric values', () => { - expect(() => dotProduct([1, null, 3], [4, 5, 6])).toThrow( + expect(() => arrayOperation([1, null, 3], [4, 5, 6])).toThrow( 'Invalid arrays structure mismatch' ); // Should reject null values - expect(() => dotProduct([1, undefined, 3], [4, 5, 6])).toThrow( + expect(() => arrayOperation([1, undefined, 3], [4, 5, 6])).toThrow( 'Invalid arrays structure mismatch' ); // Should reject undefined values - expect(() => dotProduct([1, 2, 3], [4, null, 6])).toThrow( + expect(() => arrayOperation([1, 2, 3], [4, null, 6])).toThrow( 'Invalid arrays structure mismatch' ); // Should reject null values - expect(() => dotProduct(['1', '2'], ['3', '4'])).toThrow( + expect(() => arrayOperation(['1', '2'], ['3', '4'])).toThrow( 'Invalid arrays structure mismatch' ); // Should reject string numbers - expect(() => dotProduct([1, '2'], [3, '4'])).toThrow('Invalid arrays structure mismatch'); // Should reject mixed types - expect(() => dotProduct([1, 'invalid', 3], [4, 5, 6])).toThrow( + expect(() => arrayOperation([1, '2'], [3, '4'])).toThrow( + 'Invalid arrays structure mismatch' + ); // Should reject mixed types + expect(() => arrayOperation([1, 'invalid', 3], [4, 5, 6])).toThrow( 'Invalid arrays structure mismatch' ); // Should reject invalid strings - expect(() => dotProduct([1, {}, 3], [4, 5, 6])).toThrow( + expect(() => arrayOperation([1, {}, 3], [4, 5, 6])).toThrow( 'Invalid arrays structure mismatch' ); // Should reject objects - expect(() => dotProduct([1, [], 3], [4, 5, 6])).toThrow( + expect(() => arrayOperation([1, [], 3], [4, 5, 6])).toThrow( 'Invalid arrays structure mismatch' - ); // Should reject arrays as values + ); // Should reject nested arrays in wrong positions }); it('should throw error for mixed flat and nested arrays with different structures', () => { - expect(() => dotProduct([1, 2], [[3], 4])).toThrow('Invalid arrays structure mismatch'); // Different structures - should reject - expect(() => dotProduct([[1], 2], [3, 4])).toThrow('Invalid arrays structure mismatch'); // Different structures - should reject + expect(() => arrayOperation([1, 2], [[3], 4])).toThrow('Invalid arrays structure mismatch'); // Different structures - should reject + expect(() => arrayOperation([[1], 2], [3, 4])).toThrow('Invalid arrays structure mismatch'); // Different structures - should reject }); }); describe('Basic Calculations', () => { - it('should calculate dot product for simple arrays', () => { - expect(dotProduct([1, 2, 3], [4, 5, 6])).toBe(32); // 1*4 + 2*5 + 3*6 = 32 - expect(dotProduct([2, 3], [4, 5])).toBe(23); // 2*4 + 3*5 = 23 - expect(dotProduct([1], [5])).toBe(5); // 1*5 = 5 + it('should calculate element-wise multiplication for simple arrays', () => { + expect(arrayOperation([1, 2, 3], [4, 5, 6], 'multiply')).toEqual([4, 10, 18]); + expect(arrayOperation([2, 3], [4, 5], 'multiply')).toEqual([8, 15]); + expect(arrayOperation([1], [5], 'multiply')).toEqual([5]); + }); + + it('should calculate element-wise addition for simple arrays', () => { + expect(arrayOperation([1, 2, 3], [4, 5, 6], 'add')).toEqual([5, 7, 9]); + expect(arrayOperation([2, 3], [4, 5], 'add')).toEqual([6, 8]); + expect(arrayOperation([1], [5], 'add')).toEqual([6]); + }); + + it('should calculate element-wise subtraction for simple arrays', () => { + expect(arrayOperation([10, 20, 30], [4, 5, 6], 'subtract')).toEqual([6, 15, 24]); + expect(arrayOperation([5, 8], [2, 3], 'subtract')).toEqual([3, 5]); + expect(arrayOperation([5], [1], 'subtract')).toEqual([4]); + }); + + it('should calculate element-wise division for simple arrays', () => { + expect(arrayOperation([10, 20, 30], [2, 4, 5], 'divide')).toEqual([5, 5, 6]); + expect(arrayOperation([6, 8], [2, 4], 'divide')).toEqual([3, 2]); + expect(arrayOperation([10], [5], 'divide')).toEqual([2]); }); it('should handle arrays with zeros', () => { - expect(dotProduct([1, 0, 3], [4, 5, 6])).toBe(22); // 1*4 + 0*5 + 3*6 = 22 - expect(dotProduct([0, 0, 0], [1, 2, 3])).toBe(0); // All zeros in first array + expect(arrayOperation([1, 0, 3], [4, 5, 6], 'multiply')).toEqual([4, 0, 18]); + expect(arrayOperation([0, 0, 0], [1, 2, 3], 'multiply')).toEqual([0, 0, 0]); }); it('should handle negative numbers', () => { - expect(dotProduct([-1, 2], [3, -4])).toBe(-11); // -1*3 + 2*(-4) = -11 - expect(dotProduct([-2, -3], [-4, -5])).toBe(23); // -2*(-4) + -3*(-5) = 23 + expect(arrayOperation([-1, 2], [3, -4], 'multiply')).toEqual([-3, -8]); + expect(arrayOperation([-2, -3], [-4, -5], 'multiply')).toEqual([8, 15]); + expect(arrayOperation([-1, 2], [3, -4], 'add')).toEqual([2, -2]); + }); + + it('should default to multiply when no operation specified', () => { + expect(arrayOperation([1, 2, 3], [4, 5, 6])).toEqual([4, 10, 18]); }); }); describe('Nested Array Support', () => { it('should handle nested arrays (single level)', () => { - expect(dotProduct([1, [2, 3]], [4, [5, 6]])).toBe(32); // Flattened: [1,2,3] • [4,5,6] = 32 + expect(arrayOperation([1, [2, 3]], [4, [5, 6]], 'multiply')).toEqual([4, [10, 18]]); }); it('should handle deeply nested arrays', () => { - expect(dotProduct([1, [2, [3, 4]]], [5, [6, [7, 8]]])).toBe(70); // [1,2,3,4] • [5,6,7,8] = 70 - expect(dotProduct([[[1]], [2]], [[[3]], [4]])).toBe(11); // [1,2] • [3,4] = 11 + expect(arrayOperation([1, [2, [3, 4]]], [5, [6, [7, 8]]], 'multiply')).toEqual([ + 5, + [12, [21, 32]] + ]); + expect(arrayOperation([[[1]], [2]], [[[3]], [4]], 'multiply')).toEqual([[[3]], [8]]); }); }); @@ -94,11 +126,11 @@ describe('utils', () => { // eslint-disable-next-line @typescript-eslint/no-loss-of-precision const tokenValue = [987654321.123456789]; - const result = dotProduct(votingPower, tokenValue); + const result = arrayOperation(votingPower, tokenValue, 'multiply'); // eslint-disable-next-line @typescript-eslint/no-loss-of-precision - const expected = 123456789012.456789 * 987654321.123456789; + const expected = [123456789012.456789 * 987654321.123456789]; - expect(result).toBe(expected); + expect(result).toEqual(expected); }); it('should handle small × large number multiplication (DeFi scenario)', () => { @@ -106,10 +138,9 @@ describe('utils', () => { const smallTokenValues = [1e-18, 1e-12, 1e-6]; // Wei, micro, milli units const largeVotingPower = [1e18, 1e15, 1e12]; // Large voting power values - const result = dotProduct(smallTokenValues, largeVotingPower); + const result = arrayOperation(smallTokenValues, largeVotingPower, 'multiply'); - // Should equal: 1 + 1000 + 1000000 = 1001001 - expect(result).toBe(1001001); + expect(result).toEqual([1, 1000, 1000000]); }); it('should handle maximum precision decimal numbers', () => { @@ -118,11 +149,11 @@ describe('utils', () => { const maxDecimalA = [1.1234567890123456]; const maxDecimalB = [2.9876543210987654]; - const result = dotProduct(maxDecimalA, maxDecimalB); + const result = arrayOperation(maxDecimalA, maxDecimalB, 'multiply'); // eslint-disable-next-line @typescript-eslint/no-loss-of-precision - const expected = 1.1234567890123456 * 2.9876543210987654; + const expected = [1.1234567890123456 * 2.9876543210987654]; - expect(result).toBe(expected); + expect(result).toEqual(expected); }); it('should handle underflow edge cases', () => { @@ -130,9 +161,51 @@ describe('utils', () => { const verySmallA = [1e-200]; const verySmallB = [1e-200]; - const result = dotProduct(verySmallA, verySmallB); + const result = arrayOperation(verySmallA, verySmallB, 'multiply'); - expect(result).toBe(0); // JavaScript underflow behavior + expect(result).toEqual([0]); // JavaScript underflow behavior + }); + }); + + describe('Infinity and NaN handling', () => { + it('should throw error when division by zero results in infinity', () => { + expect(() => arrayOperation([1, 2], [0, 0], 'divide')).toThrow( + 'Operation resulted in infinity or NaN' + ); + }); + + it('should throw error when multiplication results in infinity', () => { + expect(() => arrayOperation([Number.MAX_VALUE], [Number.MAX_VALUE], 'multiply')).toThrow( + 'Operation resulted in infinity or NaN' + ); + }); + + it('should throw error when operation with infinity', () => { + expect(() => arrayOperation([Infinity, 1], [1, 1], 'add')).toThrow( + 'Operation resulted in infinity or NaN' + ); // Infinity + 1 = Infinity + }); + + it('should throw error for nested arrays with infinity results', () => { + expect(() => + arrayOperation( + [ + [1, 2], + [3, 4] + ], + [ + [0, 0], + [1, 1] + ], + 'divide' + ) + ).toThrow('Operation resulted in infinity or NaN'); + }); + + it('should throw error when subtraction results in negative infinity', () => { + expect(() => arrayOperation([-Number.MAX_VALUE], [Number.MAX_VALUE], 'subtract')).toThrow( + 'Operation resulted in infinity or NaN' + ); }); }); }); From 52a90170f6ed9f631925502f0f1f1dcd22a309b6 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 6 Aug 2025 18:59:57 +0400 Subject: [PATCH 05/15] fix: remove arrayoperation --- src/helpers/utils.ts | 85 ------------- src/writer/vote.ts | 8 +- test/unit/helpers/utils.test.ts | 212 -------------------------------- 3 files changed, 3 insertions(+), 302 deletions(-) delete mode 100644 test/unit/helpers/utils.test.ts diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 6d537082..3ce0cfea 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -267,88 +267,3 @@ export function getSpaceController(space: string, network = NETWORK) { return snapshot.utils.getSpaceController(space, networkId, { broviderUrl }); } - -/** - * Performs element-wise operations on two arrays while preserving their structure. - * - * This function applies the specified operation between corresponding elements of two arrays. - * The result maintains the same nested structure as the input arrays. - * Arrays must have identical structure and contain only numbers. - * - * @param arrayA - First array of numbers. Can contain deeply nested arrays of any depth - * @param arrayB - Second array of numbers. Must have the same structure as arrayA - * @param operation - Operation to perform on corresponding elements: - * - 'multiply': a * b (default) - * - 'add': a + b - * - 'subtract': a - b - * - 'divide': a / b (returns 0 when dividing by 0) - * @returns Array with same structure as inputs, containing results from element-wise operations - * @throws {Error} Throws 'Invalid arrays structure mismatch' if arrays have different structures or contain non-numeric values - * - * @example - * // Element-wise multiplication (default) - * arrayOperation([1, 2, 3], [4, 5, 6]) // Returns [4, 10, 18] - * - * @example - * // Element-wise addition - * arrayOperation([1, 2, 3], [10, 20, 30], 'add') // Returns [11, 22, 33] - * - * @example - * // Element-wise subtraction - * arrayOperation([10, 20, 30], [1, 2, 3], 'subtract') // Returns [9, 18, 27] - * - * @example - * // Element-wise division - * arrayOperation([10, 20, 30], [2, 4, 5], 'divide') // Returns [5, 5, 6] - * - * @example - * // Nested arrays (structure preserved) - * arrayOperation([1, [2, 3]], [10, [20, 30]], 'multiply') // Returns [10, [40, 90]] - */ -export function arrayOperation( - arrayA: any[], - arrayB: any[], - operation: 'multiply' | 'add' | 'subtract' | 'divide' = 'multiply' -): any[] { - // Recursive function to process arrays while maintaining structure - function processArrays(a: any, b: any): any { - // If both are not arrays, they should be numbers - if (!Array.isArray(a) && !Array.isArray(b) && typeof a === 'number' && typeof b === 'number') { - let result: number; - switch (operation) { - case 'multiply': - result = a * b; - break; - case 'add': - result = a + b; - break; - case 'subtract': - result = a - b; - break; - case 'divide': - result = a / b; - break; - default: - result = a * b; - } - // Throw error if result is not finite - if (!isFinite(result)) { - throw new Error('Operation resulted in infinity or NaN'); - } - return result; - } - - // If both are arrays, process each element recursively - if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) { - const results: any[] = []; - for (let i = 0; i < a.length; i++) { - results.push(processArrays(a[i], b[i])); - } - return results; - } - - throw new Error('Invalid arrays structure mismatch'); - } - - return processArrays(arrayA, arrayB); -} diff --git a/src/writer/vote.ts b/src/writer/vote.ts index 1838d08d..d3f4eb6d 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -3,7 +3,7 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { getProposal } from '../helpers/actions'; import log from '../helpers/log'; import db from '../helpers/mysql'; -import { arrayOperation, captureError, hasStrategyOverride, jsonParse } from '../helpers/utils'; +import { captureError, hasStrategyOverride, jsonParse } from '../helpers/utils'; import { updateProposalAndVotes } from '../scores'; const LAST_CB = parseInt(process.env.LAST_CB ?? '1'); @@ -123,10 +123,8 @@ export async function action(body, ipfs, receipt, id, context): Promise { let cb = 0; try { - const products = arrayOperation( - context.proposal.vp_value_by_strategy, - context.vp.vp_by_strategy, - 'multiply' + const products = context.proposal.vp_value_by_strategy.map((strategyValues, index) => + strategyValues.map((value, subIndex) => value * context.vp.vp_by_strategy[index][subIndex]) ); vp_value = products.flat(Infinity).reduce((sum, val) => sum + val, 0); cb = LAST_CB; diff --git a/test/unit/helpers/utils.test.ts b/test/unit/helpers/utils.test.ts deleted file mode 100644 index 73aed5f9..00000000 --- a/test/unit/helpers/utils.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { arrayOperation } from '../../../src/helpers/utils'; - -describe('utils', () => { - describe('arrayOperation()', () => { - describe('Input Validation', () => { - it('should throw error for invalid array inputs', () => { - expect(() => arrayOperation(null as any, [1, 2])).toThrow( - 'Invalid arrays structure mismatch' - ); - expect(() => arrayOperation([1, 2], null as any)).toThrow( - 'Invalid arrays structure mismatch' - ); - expect(() => arrayOperation(undefined as any, [1, 2])).toThrow( - 'Invalid arrays structure mismatch' - ); - expect(() => arrayOperation([1, 2], undefined as any)).toThrow( - 'Invalid arrays structure mismatch' - ); - expect(() => arrayOperation('string' as any, [1, 2])).toThrow( - 'Invalid arrays structure mismatch' - ); - expect(() => arrayOperation([1, 2], 'string' as any)).toThrow( - 'Invalid arrays structure mismatch' - ); - }); - - it('should throw error for arrays of different lengths', () => { - expect(() => arrayOperation([1, 2], [3])).toThrow('Invalid arrays structure mismatch'); - expect(() => arrayOperation([1], [2, 3, 4])).toThrow('Invalid arrays structure mismatch'); - expect(() => arrayOperation([], [1, 2])).toThrow('Invalid arrays structure mismatch'); // Empty vs non-empty - }); - - it('should throw error for arrays with non-numeric values', () => { - expect(() => arrayOperation([1, null, 3], [4, 5, 6])).toThrow( - 'Invalid arrays structure mismatch' - ); // Should reject null values - expect(() => arrayOperation([1, undefined, 3], [4, 5, 6])).toThrow( - 'Invalid arrays structure mismatch' - ); // Should reject undefined values - expect(() => arrayOperation([1, 2, 3], [4, null, 6])).toThrow( - 'Invalid arrays structure mismatch' - ); // Should reject null values - expect(() => arrayOperation(['1', '2'], ['3', '4'])).toThrow( - 'Invalid arrays structure mismatch' - ); // Should reject string numbers - expect(() => arrayOperation([1, '2'], [3, '4'])).toThrow( - 'Invalid arrays structure mismatch' - ); // Should reject mixed types - expect(() => arrayOperation([1, 'invalid', 3], [4, 5, 6])).toThrow( - 'Invalid arrays structure mismatch' - ); // Should reject invalid strings - expect(() => arrayOperation([1, {}, 3], [4, 5, 6])).toThrow( - 'Invalid arrays structure mismatch' - ); // Should reject objects - expect(() => arrayOperation([1, [], 3], [4, 5, 6])).toThrow( - 'Invalid arrays structure mismatch' - ); // Should reject nested arrays in wrong positions - }); - - it('should throw error for mixed flat and nested arrays with different structures', () => { - expect(() => arrayOperation([1, 2], [[3], 4])).toThrow('Invalid arrays structure mismatch'); // Different structures - should reject - expect(() => arrayOperation([[1], 2], [3, 4])).toThrow('Invalid arrays structure mismatch'); // Different structures - should reject - }); - }); - - describe('Basic Calculations', () => { - it('should calculate element-wise multiplication for simple arrays', () => { - expect(arrayOperation([1, 2, 3], [4, 5, 6], 'multiply')).toEqual([4, 10, 18]); - expect(arrayOperation([2, 3], [4, 5], 'multiply')).toEqual([8, 15]); - expect(arrayOperation([1], [5], 'multiply')).toEqual([5]); - }); - - it('should calculate element-wise addition for simple arrays', () => { - expect(arrayOperation([1, 2, 3], [4, 5, 6], 'add')).toEqual([5, 7, 9]); - expect(arrayOperation([2, 3], [4, 5], 'add')).toEqual([6, 8]); - expect(arrayOperation([1], [5], 'add')).toEqual([6]); - }); - - it('should calculate element-wise subtraction for simple arrays', () => { - expect(arrayOperation([10, 20, 30], [4, 5, 6], 'subtract')).toEqual([6, 15, 24]); - expect(arrayOperation([5, 8], [2, 3], 'subtract')).toEqual([3, 5]); - expect(arrayOperation([5], [1], 'subtract')).toEqual([4]); - }); - - it('should calculate element-wise division for simple arrays', () => { - expect(arrayOperation([10, 20, 30], [2, 4, 5], 'divide')).toEqual([5, 5, 6]); - expect(arrayOperation([6, 8], [2, 4], 'divide')).toEqual([3, 2]); - expect(arrayOperation([10], [5], 'divide')).toEqual([2]); - }); - - it('should handle arrays with zeros', () => { - expect(arrayOperation([1, 0, 3], [4, 5, 6], 'multiply')).toEqual([4, 0, 18]); - expect(arrayOperation([0, 0, 0], [1, 2, 3], 'multiply')).toEqual([0, 0, 0]); - }); - - it('should handle negative numbers', () => { - expect(arrayOperation([-1, 2], [3, -4], 'multiply')).toEqual([-3, -8]); - expect(arrayOperation([-2, -3], [-4, -5], 'multiply')).toEqual([8, 15]); - expect(arrayOperation([-1, 2], [3, -4], 'add')).toEqual([2, -2]); - }); - - it('should default to multiply when no operation specified', () => { - expect(arrayOperation([1, 2, 3], [4, 5, 6])).toEqual([4, 10, 18]); - }); - }); - - describe('Nested Array Support', () => { - it('should handle nested arrays (single level)', () => { - expect(arrayOperation([1, [2, 3]], [4, [5, 6]], 'multiply')).toEqual([4, [10, 18]]); - }); - - it('should handle deeply nested arrays', () => { - expect(arrayOperation([1, [2, [3, 4]]], [5, [6, [7, 8]]], 'multiply')).toEqual([ - 5, - [12, [21, 32]] - ]); - expect(arrayOperation([[[1]], [2]], [[[3]], [4]], 'multiply')).toEqual([[[3]], [8]]); - }); - }); - - describe('JavaScript Native Precision', () => { - it('should handle large × large number multiplication', () => { - // Test realistic large financial numbers - // eslint-disable-next-line @typescript-eslint/no-loss-of-precision - const votingPower = [123456789012.456789]; - // eslint-disable-next-line @typescript-eslint/no-loss-of-precision - const tokenValue = [987654321.123456789]; - - const result = arrayOperation(votingPower, tokenValue, 'multiply'); - // eslint-disable-next-line @typescript-eslint/no-loss-of-precision - const expected = [123456789012.456789 * 987654321.123456789]; - - expect(result).toEqual(expected); - }); - - it('should handle small × large number multiplication (DeFi scenario)', () => { - // Test small token values with large voting power - const smallTokenValues = [1e-18, 1e-12, 1e-6]; // Wei, micro, milli units - const largeVotingPower = [1e18, 1e15, 1e12]; // Large voting power values - - const result = arrayOperation(smallTokenValues, largeVotingPower, 'multiply'); - - expect(result).toEqual([1, 1000, 1000000]); - }); - - it('should handle maximum precision decimal numbers', () => { - // Test JavaScript's precision limits (~15-16 significant digits) - // eslint-disable-next-line @typescript-eslint/no-loss-of-precision - const maxDecimalA = [1.1234567890123456]; - const maxDecimalB = [2.9876543210987654]; - - const result = arrayOperation(maxDecimalA, maxDecimalB, 'multiply'); - // eslint-disable-next-line @typescript-eslint/no-loss-of-precision - const expected = [1.1234567890123456 * 2.9876543210987654]; - - expect(result).toEqual(expected); - }); - - it('should handle underflow edge cases', () => { - // Test numbers that underflow to 0 - const verySmallA = [1e-200]; - const verySmallB = [1e-200]; - - const result = arrayOperation(verySmallA, verySmallB, 'multiply'); - - expect(result).toEqual([0]); // JavaScript underflow behavior - }); - }); - - describe('Infinity and NaN handling', () => { - it('should throw error when division by zero results in infinity', () => { - expect(() => arrayOperation([1, 2], [0, 0], 'divide')).toThrow( - 'Operation resulted in infinity or NaN' - ); - }); - - it('should throw error when multiplication results in infinity', () => { - expect(() => arrayOperation([Number.MAX_VALUE], [Number.MAX_VALUE], 'multiply')).toThrow( - 'Operation resulted in infinity or NaN' - ); - }); - - it('should throw error when operation with infinity', () => { - expect(() => arrayOperation([Infinity, 1], [1, 1], 'add')).toThrow( - 'Operation resulted in infinity or NaN' - ); // Infinity + 1 = Infinity - }); - - it('should throw error for nested arrays with infinity results', () => { - expect(() => - arrayOperation( - [ - [1, 2], - [3, 4] - ], - [ - [0, 0], - [1, 1] - ], - 'divide' - ) - ).toThrow('Operation resulted in infinity or NaN'); - }); - - it('should throw error when subtraction results in negative infinity', () => { - expect(() => arrayOperation([-Number.MAX_VALUE], [Number.MAX_VALUE], 'subtract')).toThrow( - 'Operation resulted in infinity or NaN' - ); - }); - }); - }); -}); From d7e85ab93e996588b5912961e0732f40642d5660 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:37:04 +0400 Subject: [PATCH 06/15] fix: use simple computation as both array are only number --- src/writer/vote.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/writer/vote.ts b/src/writer/vote.ts index d3f4eb6d..55a987fb 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -16,6 +16,14 @@ const scoreAPIUrl = process.env.SCORE_API_URL || 'https://score.snapshot.org'; // return count > limit; // } +function getVoteValue(proposal, vote) { + 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.map((value, index) => value * vote.vp_by_strategy[index]); +} + export async function verify(body): Promise { const msg = jsonParse(body.msg); @@ -118,15 +126,11 @@ export async function action(body, ipfs, receipt, id, context): Promise { const withOverride = hasStrategyOverride(context.proposal.strategies); if (vpState === 'final' && withOverride) vpState = 'pending'; - // Compute vote value let vp_value = 0; let cb = 0; try { - const products = context.proposal.vp_value_by_strategy.map((strategyValues, index) => - strategyValues.map((value, subIndex) => value * context.vp.vp_by_strategy[index][subIndex]) - ); - vp_value = products.flat(Infinity).reduce((sum, val) => sum + val, 0); + vp_value = getVoteValue(context.proposal, context.vp); cb = LAST_CB; } catch (e: any) { capture(e, { msg, proposalId }); From 35e4f262236e630baf173c22b65686c9af17e677 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:42:16 +0400 Subject: [PATCH 07/15] refactor: move all values logic to a helper --- src/helpers/entityValue.ts | 7 +++++++ src/writer/vote.ts | 9 +-------- 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 src/helpers/entityValue.ts diff --git a/src/helpers/entityValue.ts b/src/helpers/entityValue.ts new file mode 100644 index 00000000..01bfc654 --- /dev/null +++ b/src/helpers/entityValue.ts @@ -0,0 +1,7 @@ +export function getVoteValue(proposal, vote) { + 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.map((value, index) => value * vote.vp_by_strategy[index]); +} diff --git a/src/writer/vote.ts b/src/writer/vote.ts index 55a987fb..7a37194a 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -1,6 +1,7 @@ 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'; @@ -16,14 +17,6 @@ const scoreAPIUrl = process.env.SCORE_API_URL || 'https://score.snapshot.org'; // return count > limit; // } -function getVoteValue(proposal, vote) { - 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.map((value, index) => value * vote.vp_by_strategy[index]); -} - export async function verify(body): Promise { const msg = jsonParse(body.msg); From 08cff39213b08ad6751d34f6a2745572efc6d6de Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:51:22 +0400 Subject: [PATCH 08/15] test: add tests --- src/helpers/entityValue.ts | 18 ++++++- test/unit/helpers/entityValue.test.ts | 71 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 test/unit/helpers/entityValue.test.ts diff --git a/src/helpers/entityValue.ts b/src/helpers/entityValue.ts index 01bfc654..c23d57f0 100644 --- a/src/helpers/entityValue.ts +++ b/src/helpers/entityValue.ts @@ -1,7 +1,21 @@ -export function getVoteValue(proposal, vote) { +type Vote = { + vp_by_strategy: number[]; +}; + +type Proposal = { + vp_value_by_strategy: number[]; +}; + +/** + * Calculates the total vote value in USD based on the voting power and the proposal's value per strategy. + * @returns The total vote value in USD + **/ +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.map((value, index) => value * vote.vp_by_strategy[index]); + return proposal.vp_value_by_strategy + .map((value, index) => value * vote.vp_by_strategy[index]) + .reduce((a, b) => a + b, 0); } 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); + }); +}); From 960d8cbffd2f8ba2d79daef083c5416b6fa78684 Mon Sep 17 00:00:00 2001 From: Wan <495709+wa0x6e@users.noreply.github.com> Date: Thu, 7 Aug 2025 02:04:30 +0900 Subject: [PATCH 09/15] Update src/helpers/entityValue.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/helpers/entityValue.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/entityValue.ts b/src/helpers/entityValue.ts index c23d57f0..1c03fbea 100644 --- a/src/helpers/entityValue.ts +++ b/src/helpers/entityValue.ts @@ -8,7 +8,8 @@ type Proposal = { /** * Calculates the total vote value in USD based on the voting power and the proposal's value per strategy. - * @returns The total vote value in USD + * 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) { From 7a94dd623d61dfc06100ea7cb74e18af9db8e5e3 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 6 Aug 2025 21:21:52 +0400 Subject: [PATCH 10/15] chore: remove duplicate doc --- src/helpers/entityValue.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/entityValue.ts b/src/helpers/entityValue.ts index 1c03fbea..222fdc0b 100644 --- a/src/helpers/entityValue.ts +++ b/src/helpers/entityValue.ts @@ -7,7 +7,6 @@ type Proposal = { }; /** - * Calculates the total vote value in USD based on the voting power and the proposal's value per strategy. * 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 **/ From 43ef1bb409d38d5d8960c032a3b034d5c1261a47 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Thu, 7 Aug 2025 00:10:06 +0400 Subject: [PATCH 11/15] feat: increase leaderboard vp_value on vote creation --- src/writer/vote.ts | 6 +++--- test/schema.sql | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/writer/vote.ts b/src/writer/vote.ts index 7a37194a..7ac0cf32 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -194,12 +194,12 @@ export async function action(body, ipfs, receipt, id, context): Promise { await db.queryAsync( ` INSERT INTO votes SET ?; - INSERT INTO leaderboard (space, user, vote_count, last_vote) - VALUES(?, ?, 1, ?) + INSERT INTO leaderboard (space, user, vote_count, last_vote, vp_value) + VALUES(?, ?, 1, ?, ?) ON DUPLICATE KEY UPDATE vote_count = vote_count + 1, last_vote = ?; UPDATE spaces SET vote_count = vote_count + 1 WHERE id = ?; `, - [params, msg.space, voter, created, created, msg.space] + [params, msg.space, voter, created, vp_value, created, msg.space] ); } diff --git a/test/schema.sql b/test/schema.sql index 7477fb10..d94d5664 100644 --- a/test/schema.sql +++ b/test/schema.sql @@ -190,11 +190,13 @@ CREATE TABLE leaderboard ( vote_count SMALLINT UNSIGNED NOT NULL DEFAULT '0', proposal_count SMALLINT UNSIGNED NOT NULL DEFAULT '0', last_vote BIGINT, + vp_value DECIMAL(13,3) NOT NULL DEFAULT 0.000, PRIMARY KEY user_space (user,space), INDEX space (space), INDEX vote_count (vote_count), INDEX proposal_count (proposal_count), - INDEX last_vote (last_vote) + INDEX last_vote (last_vote), + INDEX vp_value (vp_value) ); CREATE TABLE skins ( From 2172f479ffe145fa939a36a12283cf1b393b5b7d Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Thu, 7 Aug 2025 00:31:38 +0400 Subject: [PATCH 12/15] feat: decrease leaderboard vp_value on proposal deletion --- src/writer/delete-proposal.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/writer/delete-proposal.ts b/src/writer/delete-proposal.ts index 3d777acd..73ccd10b 100644 --- a/src/writer/delete-proposal.ts +++ b/src/writer/delete-proposal.ts @@ -22,7 +22,7 @@ export async function action(body): Promise { const msg = jsonParse(body.msg); const proposal = await getProposal(msg.space, msg.payload.proposal); - const voters = await db.queryAsync(`SELECT voter FROM votes WHERE proposal = ?`, [ + const voters = await db.queryAsync(`SELECT voter, vp_value FROM votes WHERE proposal = ?`, [ msg.payload.proposal ]); const id = msg.payload.proposal; @@ -53,4 +53,22 @@ export async function action(body): Promise { } await db.queryAsync(queries, parameters); + + const votersWithVpValue = voters.filter(v => v.vp_value > 0); + if (votersWithVpValue.length > 0) { + const batchSize = 1000; + for (let i = 0; i < votersWithVpValue.length; i += batchSize) { + const batch = votersWithVpValue.slice(i, i + batchSize); + const vpQueries = batch + .map( + () => + `UPDATE leaderboard SET vp_value = GREATEST(vp_value - ?, 0) WHERE user = ? AND space = ?;` + ) + .join('\n '); + + const vpParams = batch.flatMap(voter => [voter.vp_value, voter.voter, msg.space]); + + await db.queryAsync(vpQueries, vpParams); + } + } } From d6381a80d36d12d35ab1293a828a5a9e3d20cf78 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:09:09 +0400 Subject: [PATCH 13/15] fix: update leaderboard vp_value on new vote --- src/writer/vote.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/writer/vote.ts b/src/writer/vote.ts index 7ac0cf32..2a8f0b1a 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -185,6 +185,7 @@ export async function action(body, ipfs, receipt, id, context): Promise { proposalId, msg.space, created, + vp_value, voter, msg.space ] @@ -194,12 +195,20 @@ export async function action(body, ipfs, receipt, id, context): Promise { await db.queryAsync( ` INSERT INTO votes SET ?; - INSERT INTO leaderboard (space, user, vote_count, last_vote, vp_value) - VALUES(?, ?, 1, ?, ?) - ON DUPLICATE KEY UPDATE vote_count = vote_count + 1, last_vote = ?; - UPDATE spaces SET vote_count = vote_count + 1 WHERE id = ?; + INSERT INTO leaderboard ( + space, user, vote_count, last_vote, vp_value + ) VALUES ( + ?, ?, 1, ?, ? + ) + ON DUPLICATE KEY UPDATE + vote_count = vote_count + 1, + last_vote = VALUES(last_vote), + vp_value = vp_value + VALUES(vp_value); + UPDATE spaces + SET vote_count = vote_count + 1 + WHERE id = ?; `, - [params, msg.space, voter, created, vp_value, created, msg.space] + [params, msg.space, voter, created, vp_value, msg.space] ); } From 01a782bbab6215982a045195b90ba89a742f66d4 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:21:46 +0400 Subject: [PATCH 14/15] refactor: code improvement --- src/helpers/entityValue.ts | 7 ++++--- src/writer/delete-proposal.ts | 7 ++++--- src/writer/vote.ts | 30 ++++++++++++------------------ 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/helpers/entityValue.ts b/src/helpers/entityValue.ts index 222fdc0b..25e891b5 100644 --- a/src/helpers/entityValue.ts +++ b/src/helpers/entityValue.ts @@ -15,7 +15,8 @@ export function getVoteValue(proposal: Proposal, vote: Vote): number { throw new Error('invalid data to compute vote value'); } - return proposal.vp_value_by_strategy - .map((value, index) => value * vote.vp_by_strategy[index]) - .reduce((a, b) => a + b, 0); + return proposal.vp_value_by_strategy.reduce( + (sum, value, index) => sum + value * vote.vp_by_strategy[index], + 0 + ); } diff --git a/src/writer/delete-proposal.ts b/src/writer/delete-proposal.ts index 73ccd10b..4e550b90 100644 --- a/src/writer/delete-proposal.ts +++ b/src/writer/delete-proposal.ts @@ -19,6 +19,8 @@ export async function verify(body): Promise { } export async function action(body): Promise { + const BATCH_SIZE = 1000; + const msg = jsonParse(body.msg); const proposal = await getProposal(msg.space, msg.payload.proposal); @@ -56,9 +58,8 @@ export async function action(body): Promise { const votersWithVpValue = voters.filter(v => v.vp_value > 0); if (votersWithVpValue.length > 0) { - const batchSize = 1000; - for (let i = 0; i < votersWithVpValue.length; i += batchSize) { - const batch = votersWithVpValue.slice(i, i + batchSize); + for (let i = 0; i < votersWithVpValue.length; i += BATCH_SIZE) { + const batch = votersWithVpValue.slice(i, i + BATCH_SIZE); const vpQueries = batch .map( () => diff --git a/src/writer/vote.ts b/src/writer/vote.ts index 2a8f0b1a..3bd442c8 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -119,14 +119,16 @@ export async function action(body, ipfs, receipt, id, context): Promise { const withOverride = hasStrategyOverride(context.proposal.strategies); if (vpState === 'final' && withOverride) vpState = 'pending'; - let vp_value = 0; + // Get proposal voting power value + // Value is set on creation, and not updated on vote update + let vpValue = 0; let cb = 0; try { - vp_value = getVoteValue(context.proposal, context.vp); + vpValue = getVoteValue(context.proposal, context.vp); cb = LAST_CB; } catch (e: any) { - capture(e, { msg, proposalId }); + capture(e, { msg, proposalId, context }); } const params = { @@ -143,7 +145,7 @@ 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, - vp_value, + vp_value: vpValue, cb }; @@ -185,7 +187,7 @@ export async function action(body, ipfs, receipt, id, context): Promise { proposalId, msg.space, created, - vp_value, + vpValue, voter, msg.space ] @@ -195,20 +197,12 @@ export async function action(body, ipfs, receipt, id, context): Promise { await db.queryAsync( ` INSERT INTO votes SET ?; - INSERT INTO leaderboard ( - space, user, vote_count, last_vote, vp_value - ) VALUES ( - ?, ?, 1, ?, ? - ) - ON DUPLICATE KEY UPDATE - vote_count = vote_count + 1, - last_vote = VALUES(last_vote), - vp_value = vp_value + VALUES(vp_value); - UPDATE spaces - SET vote_count = vote_count + 1 - WHERE id = ?; + INSERT INTO leaderboard (space, user, vote_count, last_vote, vp_value) + VALUES(?, ?, 1, ?, ?) + ON DUPLICATE KEY UPDATE vote_count = vote_count + 1, last_vote = VALUES(last_vote), vp_value = vp_value + VALUES(vp_value); + UPDATE spaces SET vote_count = vote_count + 1 WHERE id = ?; `, - [params, msg.space, voter, created, vp_value, msg.space] + [params, msg.space, voter, created, vpValue, msg.space] ); } From 207810ec891d4c95718d1615247a60899e82ad1c Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:10:34 +0400 Subject: [PATCH 15/15] refactor: code improvement --- src/helpers/entityValue.ts | 7 ++++--- src/writer/vote.ts | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/helpers/entityValue.ts b/src/helpers/entityValue.ts index 222fdc0b..25e891b5 100644 --- a/src/helpers/entityValue.ts +++ b/src/helpers/entityValue.ts @@ -15,7 +15,8 @@ export function getVoteValue(proposal: Proposal, vote: Vote): number { throw new Error('invalid data to compute vote value'); } - return proposal.vp_value_by_strategy - .map((value, index) => value * vote.vp_by_strategy[index]) - .reduce((a, b) => a + b, 0); + 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 7a37194a..2d5b76bb 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -119,14 +119,16 @@ export async function action(body, ipfs, receipt, id, context): Promise { const withOverride = hasStrategyOverride(context.proposal.strategies); if (vpState === 'final' && withOverride) vpState = 'pending'; - let vp_value = 0; + // Get proposal voting power value + // Value is set on creation, and not updated on vote update + let vpValue = 0; let cb = 0; try { - vp_value = getVoteValue(context.proposal, context.vp); + vpValue = getVoteValue(context.proposal, context.vp); cb = LAST_CB; } catch (e: any) { - capture(e, { msg, proposalId }); + capture(e, { msg, proposalId, context }); } const params = { @@ -143,7 +145,7 @@ 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, - vp_value, + vp_value: vpValue, cb };