Skip to content

Commit 4383ded

Browse files
committed
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
1 parent 228bf79 commit 4383ded

File tree

5 files changed

+250
-2
lines changed

5 files changed

+250
-2
lines changed

src/helpers/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export async function getProposal(space, id) {
9292
params: {}
9393
};
9494
proposal.choices = jsonParse(proposal.choices);
95+
proposal.vp_value_by_strategy = jsonParse(proposal.vp_value_by_strategy, []);
9596

9697
return proposal;
9798
}

src/helpers/utils.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,116 @@ export function getSpaceController(space: string, network = NETWORK) {
266266

267267
return snapshot.utils.getSpaceController(space, networkId, { broviderUrl });
268268
}
269+
270+
/**
271+
* Recursively checks if two arrays have the exact same structure,
272+
* including nesting levels at corresponding positions, and contain only valid numbers.
273+
*
274+
* @param arrayA - First array
275+
* @param arrayB - Second array
276+
* @returns True if structures match exactly and contain only numbers, false otherwise
277+
*/
278+
function arraysHaveSameStructure(arrayA: any, arrayB: any): boolean {
279+
// Both must be arrays or both must not be arrays
280+
if (Array.isArray(arrayA) !== Array.isArray(arrayB)) {
281+
return false;
282+
}
283+
284+
// If neither is an array, check they are valid numeric values
285+
if (!Array.isArray(arrayA)) {
286+
return (
287+
typeof arrayA === 'number' && !isNaN(arrayA) && typeof arrayB === 'number' && !isNaN(arrayB)
288+
);
289+
}
290+
291+
// Both are arrays - check they have the same length
292+
if (arrayA.length !== arrayB.length) {
293+
return false;
294+
}
295+
296+
// Recursively check each element has the same structure and valid content
297+
for (let i = 0; i < arrayA.length; i++) {
298+
if (!arraysHaveSameStructure(arrayA[i], arrayB[i])) {
299+
return false;
300+
}
301+
}
302+
303+
return true;
304+
}
305+
306+
/**
307+
* Validates and prepares arrays for dot product calculation.
308+
* Ensures arrays have identical structure and nesting patterns.
309+
*
310+
* @param arrayA - First array
311+
* @param arrayB - Second array
312+
* @returns Object with flattened arrays, or null if invalid or structure mismatch
313+
*/
314+
function validateAndFlattenArrays(
315+
arrayA: any,
316+
arrayB: any
317+
): { flatArrayA: any[]; flatArrayB: any[] } | null {
318+
if (!Array.isArray(arrayA) || !Array.isArray(arrayB)) {
319+
return null;
320+
}
321+
322+
// Check for exact structural match
323+
if (!arraysHaveSameStructure(arrayA, arrayB)) {
324+
return null;
325+
}
326+
327+
// Structure matches, safe to flatten
328+
const flatArrayA = arrayA.flat(Infinity);
329+
const flatArrayB = arrayB.flat(Infinity);
330+
331+
return { flatArrayA, flatArrayB };
332+
}
333+
334+
/**
335+
* Computes the dot product of two arrays by multiplying corresponding elements and summing the results.
336+
*
337+
* This function performs a dot product calculation between two arrays of numbers using
338+
* JavaScript's native arithmetic. Both arrays are flattened to handle nested structures
339+
* of unlimited depth before calculation. Arrays must have identical structure and contain only numbers.
340+
*
341+
* @param arrayA - First array of numbers. Can contain deeply nested arrays of any depth
342+
* @param arrayB - Second array of numbers. Must have the same structure as arrayA after flattening
343+
* @returns The computed dot product as a number. Returns 0 if arrays are invalid or mismatched.
344+
*
345+
* @example
346+
* // Simple arrays
347+
* dotProduct([1, 2, 3], [10, 20, 30]) // Returns 140 (1*10 + 2*20 + 3*30)
348+
*
349+
* @example
350+
* // Nested arrays (2 levels)
351+
* dotProduct([1, [2, 3]], [10, [20, 30]]) // Returns 140 (1*10 + 2*20 + 3*30)
352+
*
353+
* @example
354+
* // Financial calculations
355+
* dotProduct([1.833444691890596], [1000.123456789]) // Uses JavaScript precision
356+
*/
357+
export function dotProduct(arrayA: any[], arrayB: any[]): number {
358+
const validation = validateAndFlattenArrays(arrayA, arrayB);
359+
if (!validation) {
360+
throw new Error('Invalid arrays structure mismatch');
361+
}
362+
363+
const { flatArrayA, flatArrayB } = validation;
364+
365+
// Use pure JavaScript arithmetic for all calculations
366+
let sum = 0;
367+
368+
for (let i = 0; i < flatArrayA.length; i++) {
369+
const numA = flatArrayA[i];
370+
const numB = flatArrayB[i];
371+
372+
const product = numA * numB;
373+
374+
// Only add finite numbers to avoid NaN propagation
375+
if (isFinite(product)) {
376+
sum += product;
377+
}
378+
}
379+
380+
return sum;
381+
}

src/writer/vote.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { capture } from '@snapshot-labs/snapshot-sentry';
12
import snapshot from '@snapshot-labs/snapshot.js';
23
import { getProposal } from '../helpers/actions';
34
import log from '../helpers/log';
45
import db from '../helpers/mysql';
5-
import { captureError, hasStrategyOverride, jsonParse } from '../helpers/utils';
6+
import { captureError, dotProduct, hasStrategyOverride, jsonParse } from '../helpers/utils';
67
import { updateProposalAndVotes } from '../scores';
78

9+
const LAST_VOTE_CB = 1;
810
const scoreAPIUrl = process.env.SCORE_API_URL || 'https://score.snapshot.org';
911

1012
// async function isLimitReached(space) {
@@ -116,6 +118,17 @@ export async function action(body, ipfs, receipt, id, context): Promise<void> {
116118
const withOverride = hasStrategyOverride(context.proposal.strategies);
117119
if (vpState === 'final' && withOverride) vpState = 'pending';
118120

121+
// Compute vote value
122+
let vp_value = 0;
123+
let cb = 0;
124+
125+
try {
126+
vp_value = dotProduct(context.proposal.vp_value_by_strategy, context.vp.vp_by_strategy);
127+
cb = LAST_VOTE_CB;
128+
} catch (e: any) {
129+
capture(e, { msg, proposalId });
130+
}
131+
119132
const params = {
120133
id,
121134
ipfs,
@@ -130,7 +143,8 @@ export async function action(body, ipfs, receipt, id, context): Promise<void> {
130143
vp: context.vp.vp,
131144
vp_by_strategy: JSON.stringify(context.vp.vp_by_strategy),
132145
vp_state: vpState,
133-
cb: 0
146+
vp_value,
147+
cb
134148
};
135149

136150
// Check if voter already voted

test/schema.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ CREATE TABLE votes (
100100
vp DECIMAL(64,30) NOT NULL,
101101
vp_by_strategy JSON NOT NULL,
102102
vp_state VARCHAR(24) NOT NULL,
103+
vp_value DECIMAL(13,3) NOT NULL DEFAULT 0.000,
103104
cb INT(11) NOT NULL,
104105
PRIMARY KEY (voter, space, proposal),
105106
UNIQUE KEY id (id),
@@ -111,6 +112,7 @@ CREATE TABLE votes (
111112
INDEX app (app),
112113
INDEX vp (vp),
113114
INDEX vp_state (vp_state),
115+
INDEX vp_value (vp_value),
114116
INDEX cb (cb)
115117
);
116118

test/unit/helpers/utils.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { dotProduct } from '../../../src/helpers/utils';
2+
3+
describe('utils', () => {
4+
describe('dotProduct()', () => {
5+
describe('Input Validation', () => {
6+
it('should return 0 for invalid array inputs', () => {
7+
expect(dotProduct(null as any, [1, 2])).toBe(0);
8+
expect(dotProduct([1, 2], null as any)).toBe(0);
9+
expect(dotProduct(undefined as any, [1, 2])).toBe(0);
10+
expect(dotProduct([1, 2], undefined as any)).toBe(0);
11+
expect(dotProduct('string' as any, [1, 2])).toBe(0);
12+
expect(dotProduct([1, 2], 'string' as any)).toBe(0);
13+
});
14+
15+
it('should return 0 for arrays of different lengths', () => {
16+
expect(dotProduct([1, 2], [3])).toBe(0);
17+
expect(dotProduct([1], [2, 3, 4])).toBe(0);
18+
expect(dotProduct([], [1, 2])).toBe(0); // Empty vs non-empty
19+
expect(dotProduct([], [])).toBe(0); // Both empty
20+
});
21+
22+
it('should reject arrays with non-numeric values', () => {
23+
expect(dotProduct([1, null, 3], [4, 5, 6])).toBe(0); // Should reject null values
24+
expect(dotProduct([1, undefined, 3], [4, 5, 6])).toBe(0); // Should reject undefined values
25+
expect(dotProduct([1, 2, 3], [4, null, 6])).toBe(0); // Should reject null values
26+
expect(dotProduct(['1', '2'], ['3', '4'])).toBe(0); // Should reject string numbers
27+
expect(dotProduct([1, '2'], [3, '4'])).toBe(0); // Should reject mixed types
28+
expect(dotProduct([1, 'invalid', 3], [4, 5, 6])).toBe(0); // Should reject invalid strings
29+
expect(dotProduct([1, {}, 3], [4, 5, 6])).toBe(0); // Should reject objects
30+
expect(dotProduct([1, [], 3], [4, 5, 6])).toBe(0); // Should reject arrays as values
31+
});
32+
33+
it('should reject mixed flat and nested arrays with different structures', () => {
34+
expect(dotProduct([1, 2], [[3], 4])).toBe(0); // Different structures - should reject
35+
expect(dotProduct([[1], 2], [3, 4])).toBe(0); // Different structures - should reject
36+
});
37+
});
38+
39+
describe('Basic Calculations', () => {
40+
it('should calculate dot product for simple arrays', () => {
41+
expect(dotProduct([1, 2, 3], [4, 5, 6])).toBe(32); // 1*4 + 2*5 + 3*6 = 32
42+
expect(dotProduct([2, 3], [4, 5])).toBe(23); // 2*4 + 3*5 = 23
43+
expect(dotProduct([1], [5])).toBe(5); // 1*5 = 5
44+
});
45+
46+
it('should handle arrays with zeros', () => {
47+
expect(dotProduct([1, 0, 3], [4, 5, 6])).toBe(22); // 1*4 + 0*5 + 3*6 = 22
48+
expect(dotProduct([0, 0, 0], [1, 2, 3])).toBe(0); // All zeros in first array
49+
});
50+
51+
it('should handle negative numbers', () => {
52+
expect(dotProduct([-1, 2], [3, -4])).toBe(-11); // -1*3 + 2*(-4) = -11
53+
expect(dotProduct([-2, -3], [-4, -5])).toBe(23); // -2*(-4) + -3*(-5) = 23
54+
});
55+
});
56+
57+
describe('Nested Array Support', () => {
58+
it('should handle nested arrays (single level)', () => {
59+
expect(dotProduct([1, [2, 3]], [4, [5, 6]])).toBe(32); // Flattened: [1,2,3] • [4,5,6] = 32
60+
});
61+
62+
it('should handle deeply nested arrays', () => {
63+
expect(dotProduct([1, [2, [3, 4]]], [5, [6, [7, 8]]])).toBe(70); // [1,2,3,4] • [5,6,7,8] = 70
64+
expect(dotProduct([[[1]], [2]], [[[3]], [4]])).toBe(11); // [1,2] • [3,4] = 11
65+
});
66+
});
67+
68+
describe('JavaScript Native Precision', () => {
69+
it('should handle large × large number multiplication', () => {
70+
// Test realistic large financial numbers
71+
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
72+
const votingPower = [123456789012.456789];
73+
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
74+
const tokenValue = [987654321.123456789];
75+
76+
const result = dotProduct(votingPower, tokenValue);
77+
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
78+
const expected = 123456789012.456789 * 987654321.123456789;
79+
80+
expect(result).toBe(expected);
81+
});
82+
83+
it('should handle small × large number multiplication (DeFi scenario)', () => {
84+
// Test small token values with large voting power
85+
const smallTokenValues = [1e-18, 1e-12, 1e-6]; // Wei, micro, milli units
86+
const largeVotingPower = [1e18, 1e15, 1e12]; // Large voting power values
87+
88+
const result = dotProduct(smallTokenValues, largeVotingPower);
89+
90+
// Should equal: 1 + 1000 + 1000000 = 1001001
91+
expect(result).toBe(1001001);
92+
});
93+
94+
it('should handle maximum precision decimal numbers', () => {
95+
// Test JavaScript's precision limits (~15-16 significant digits)
96+
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
97+
const maxDecimalA = [1.1234567890123456];
98+
const maxDecimalB = [2.9876543210987654];
99+
100+
const result = dotProduct(maxDecimalA, maxDecimalB);
101+
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
102+
const expected = 1.1234567890123456 * 2.9876543210987654;
103+
104+
expect(result).toBe(expected);
105+
});
106+
107+
it('should handle underflow edge cases', () => {
108+
// Test numbers that underflow to 0
109+
const verySmallA = [1e-200];
110+
const verySmallB = [1e-200];
111+
112+
const result = dotProduct(verySmallA, verySmallB);
113+
114+
expect(result).toBe(0); // JavaScript underflow behavior
115+
});
116+
});
117+
});
118+
});

0 commit comments

Comments
 (0)