@snapshot-labs/snapshot.js 0.12.52 → 0.12.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/snapshot.cjs.js +78 -34
- package/dist/snapshot.esm.js +78 -34
- package/dist/snapshot.min.js +1 -1
- package/package.json +1 -1
- package/src/voting/__snapshots__/copeland.spec.js.snap +66 -33
- package/src/voting/copeland.spec.js +101 -0
- package/src/voting/copeland.ts +125 -34
package/package.json
CHANGED
|
@@ -2,27 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`Partial ranking 1`] = `
|
|
4
4
|
[
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
1.6666666666666665,
|
|
6
|
+
2.5,
|
|
7
|
+
0.8333333333333333,
|
|
8
8
|
0,
|
|
9
9
|
]
|
|
10
10
|
`;
|
|
11
11
|
|
|
12
12
|
exports[`getScores 1`] = `
|
|
13
13
|
[
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
1.6666666666666667,
|
|
15
|
+
1.6666666666666667,
|
|
16
|
+
0.6666666666666666,
|
|
17
17
|
0,
|
|
18
18
|
]
|
|
19
19
|
`;
|
|
20
20
|
|
|
21
21
|
exports[`getScores 2`] = `
|
|
22
22
|
[
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
1.6666666666666667,
|
|
24
|
+
1.6666666666666667,
|
|
25
|
+
0.6666666666666666,
|
|
26
26
|
0,
|
|
27
27
|
]
|
|
28
28
|
`;
|
|
@@ -45,16 +45,32 @@ exports[`getScores 4`] = `
|
|
|
45
45
|
]
|
|
46
46
|
`;
|
|
47
47
|
|
|
48
|
+
exports[`getScores with 0 votes 1`] = `
|
|
49
|
+
[
|
|
50
|
+
0,
|
|
51
|
+
0,
|
|
52
|
+
0,
|
|
53
|
+
]
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
exports[`getScores with mixed voting powers 1`] = `
|
|
57
|
+
[
|
|
58
|
+
668334.3333333333,
|
|
59
|
+
334167.1666666666,
|
|
60
|
+
0,
|
|
61
|
+
]
|
|
62
|
+
`;
|
|
63
|
+
|
|
48
64
|
exports[`getScoresByStrategy 1`] = `
|
|
49
65
|
[
|
|
50
66
|
[
|
|
51
|
-
|
|
67
|
+
1.6666666666666667,
|
|
52
68
|
],
|
|
53
69
|
[
|
|
54
|
-
|
|
70
|
+
1.6666666666666667,
|
|
55
71
|
],
|
|
56
72
|
[
|
|
57
|
-
|
|
73
|
+
0.6666666666666666,
|
|
58
74
|
],
|
|
59
75
|
[
|
|
60
76
|
0,
|
|
@@ -65,13 +81,13 @@ exports[`getScoresByStrategy 1`] = `
|
|
|
65
81
|
exports[`getScoresByStrategy 2`] = `
|
|
66
82
|
[
|
|
67
83
|
[
|
|
68
|
-
|
|
84
|
+
1.6666666666666667,
|
|
69
85
|
],
|
|
70
86
|
[
|
|
71
|
-
|
|
87
|
+
1.6666666666666667,
|
|
72
88
|
],
|
|
73
89
|
[
|
|
74
|
-
|
|
90
|
+
0.6666666666666666,
|
|
75
91
|
],
|
|
76
92
|
[
|
|
77
93
|
0,
|
|
@@ -82,19 +98,19 @@ exports[`getScoresByStrategy 2`] = `
|
|
|
82
98
|
exports[`getScoresByStrategy 3`] = `
|
|
83
99
|
[
|
|
84
100
|
[
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
1.6666666666666667,
|
|
102
|
+
1.6666666666666667,
|
|
103
|
+
1.6666666666666667,
|
|
88
104
|
],
|
|
89
105
|
[
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
1.6666666666666667,
|
|
107
|
+
1.6666666666666667,
|
|
108
|
+
1.6666666666666667,
|
|
93
109
|
],
|
|
94
110
|
[
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
0.6666666666666666,
|
|
112
|
+
0.6666666666666666,
|
|
113
|
+
0.6666666666666666,
|
|
98
114
|
],
|
|
99
115
|
[
|
|
100
116
|
0,
|
|
@@ -107,19 +123,19 @@ exports[`getScoresByStrategy 3`] = `
|
|
|
107
123
|
exports[`getScoresByStrategy 4`] = `
|
|
108
124
|
[
|
|
109
125
|
[
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
1.6666666666666667,
|
|
127
|
+
1.6666666666666667,
|
|
128
|
+
1.6666666666666667,
|
|
113
129
|
],
|
|
114
130
|
[
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
131
|
+
1.6666666666666667,
|
|
132
|
+
1.6666666666666667,
|
|
133
|
+
1.6666666666666667,
|
|
118
134
|
],
|
|
119
135
|
[
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
0.6666666666666666,
|
|
137
|
+
0.6666666666666666,
|
|
138
|
+
0.6666666666666666,
|
|
123
139
|
],
|
|
124
140
|
[
|
|
125
141
|
0,
|
|
@@ -129,6 +145,23 @@ exports[`getScoresByStrategy 4`] = `
|
|
|
129
145
|
]
|
|
130
146
|
`;
|
|
131
147
|
|
|
148
|
+
exports[`getScoresByStrategy normalizes correctly 1`] = `
|
|
149
|
+
[
|
|
150
|
+
[
|
|
151
|
+
7.5,
|
|
152
|
+
22.5,
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
7.5,
|
|
156
|
+
22.5,
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
7.5,
|
|
160
|
+
22.5,
|
|
161
|
+
],
|
|
162
|
+
]
|
|
163
|
+
`;
|
|
164
|
+
|
|
132
165
|
exports[`getScoresTotal 1`] = `4`;
|
|
133
166
|
|
|
134
167
|
exports[`getScoresTotal 2`] = `12`;
|
|
@@ -49,6 +49,44 @@ const votesWithInvalidChoices2 = () => {
|
|
|
49
49
|
return [...invalidVotes, ...example2().votes];
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
// Helper function to create example with decimal voting powers
|
|
53
|
+
const exampleWithDecimals = () => {
|
|
54
|
+
const proposal = {
|
|
55
|
+
choices: ['Alice', 'Bob', 'Carol']
|
|
56
|
+
};
|
|
57
|
+
const strategies = [{ name: 'ticket', network: '1', params: {} }];
|
|
58
|
+
const votes = [
|
|
59
|
+
{ choice: [1, 2, 3], balance: 1.5, scores: [1.5] },
|
|
60
|
+
{ choice: [2, 1, 3], balance: 2.75, scores: [2.75] },
|
|
61
|
+
{ choice: [3, 2, 1], balance: 0.25, scores: [0.25] }
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
proposal,
|
|
66
|
+
strategies,
|
|
67
|
+
votes
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Helper function to create example with high voting powers
|
|
72
|
+
const exampleWithHighPowers = () => {
|
|
73
|
+
const proposal = {
|
|
74
|
+
choices: ['Alice', 'Bob', 'Carol']
|
|
75
|
+
};
|
|
76
|
+
const strategies = [{ name: 'ticket', network: '1', params: {} }];
|
|
77
|
+
const votes = [
|
|
78
|
+
{ choice: [1, 2, 3], balance: 1000000, scores: [1000000] },
|
|
79
|
+
{ choice: [2, 1, 3], balance: 2500000, scores: [2500000] },
|
|
80
|
+
{ choice: [3, 2, 1], balance: 1500000, scores: [1500000] }
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
proposal,
|
|
85
|
+
strategies,
|
|
86
|
+
votes
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
52
90
|
// Test cases for getScores method
|
|
53
91
|
test.each([
|
|
54
92
|
[example.proposal, example.votes, example.strategies],
|
|
@@ -81,6 +119,42 @@ test.each([
|
|
|
81
119
|
expect(copeland.getScoresByStrategy()).toMatchSnapshot();
|
|
82
120
|
});
|
|
83
121
|
|
|
122
|
+
// Add test for verifying strategy normalization
|
|
123
|
+
test('getScoresByStrategy normalizes correctly', () => {
|
|
124
|
+
const proposal = {
|
|
125
|
+
choices: ['Alice', 'Bob', 'Carol']
|
|
126
|
+
};
|
|
127
|
+
const strategies = [
|
|
128
|
+
{ name: 'ticket', network: '1', params: {} },
|
|
129
|
+
{ name: 'erc20-balance-of', network: '1', params: {} }
|
|
130
|
+
];
|
|
131
|
+
const votes = [
|
|
132
|
+
{ choice: [1, 2, 3], balance: 10, scores: [5, 15] },
|
|
133
|
+
{ choice: [2, 3, 1], balance: 20, scores: [10, 30] },
|
|
134
|
+
{ choice: [3, 1, 2], balance: 15, scores: [7.5, 22.5] }
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const copeland = new CopelandVoting(proposal, votes, strategies, [1]);
|
|
138
|
+
|
|
139
|
+
const scoresByStrategy = copeland.getScoresByStrategy();
|
|
140
|
+
expect(scoresByStrategy).toMatchSnapshot();
|
|
141
|
+
|
|
142
|
+
// Verify totals per strategy
|
|
143
|
+
const strategyTotals = [22.5, 67.5]; // Sum of all votes per strategy
|
|
144
|
+
|
|
145
|
+
// Sum the scores for each strategy
|
|
146
|
+
const strategyTotalResults = [0, 0];
|
|
147
|
+
for (let i = 0; i < strategies.length; i++) {
|
|
148
|
+
for (let j = 0; j < proposal.choices.length; j++) {
|
|
149
|
+
strategyTotalResults[i] += scoresByStrategy[j][i];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Verify total voting power is preserved for each strategy
|
|
154
|
+
expect(strategyTotalResults[0]).toBeCloseTo(strategyTotals[0], 5);
|
|
155
|
+
expect(strategyTotalResults[1]).toBeCloseTo(strategyTotals[1], 5);
|
|
156
|
+
});
|
|
157
|
+
|
|
84
158
|
// Test cases for getScoresTotal method
|
|
85
159
|
test.each([
|
|
86
160
|
[example.proposal, example.votes, example.strategies],
|
|
@@ -127,3 +201,30 @@ test('Partial ranking', () => {
|
|
|
127
201
|
);
|
|
128
202
|
expect(copeland.getScores()).toMatchSnapshot();
|
|
129
203
|
});
|
|
204
|
+
|
|
205
|
+
test('getScores with mixed voting powers', () => {
|
|
206
|
+
const proposal = {
|
|
207
|
+
choices: ['Alice', 'Bob', 'Carol']
|
|
208
|
+
};
|
|
209
|
+
const votes = [
|
|
210
|
+
{ choice: [1, 2, 3], balance: 1000000.75, scores: [1000000.75] },
|
|
211
|
+
{ choice: [2, 3, 1], balance: 0.25, scores: [0.25] },
|
|
212
|
+
{ choice: [3, 1, 2], balance: 2500.5, scores: [2500.5] }
|
|
213
|
+
];
|
|
214
|
+
const copeland = new CopelandVoting(proposal, votes, example.strategies, [1]);
|
|
215
|
+
const scores = copeland.getScores();
|
|
216
|
+
expect(scores).toMatchSnapshot();
|
|
217
|
+
// Verify total voting power is preserved
|
|
218
|
+
expect(scores.reduce((a, b) => a + b)).toBeCloseTo(1002501.5, 5);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('getScores with 0 votes', () => {
|
|
222
|
+
const proposal = {
|
|
223
|
+
choices: ['Alice', 'Bob', 'Carol']
|
|
224
|
+
};
|
|
225
|
+
const votes = [];
|
|
226
|
+
const copeland = new CopelandVoting(proposal, votes, example.strategies, [1]);
|
|
227
|
+
const scores = copeland.getScores();
|
|
228
|
+
expect(scores).toMatchSnapshot();
|
|
229
|
+
// Verify total voting power is preserved
|
|
230
|
+
});
|
package/src/voting/copeland.ts
CHANGED
|
@@ -57,37 +57,64 @@ export default class CopelandVoting {
|
|
|
57
57
|
const pairwiseComparisons = Array.from({ length: choicesCount }, () =>
|
|
58
58
|
Array(choicesCount).fill(0)
|
|
59
59
|
);
|
|
60
|
+
const totalVotingPower = this.getScoresTotal();
|
|
60
61
|
|
|
61
62
|
// Calculate pairwise comparisons
|
|
62
63
|
for (const vote of validVotes) {
|
|
63
|
-
for (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
for (
|
|
65
|
+
let currentRank = 0;
|
|
66
|
+
currentRank < vote.choice.length;
|
|
67
|
+
currentRank++
|
|
68
|
+
) {
|
|
69
|
+
for (
|
|
70
|
+
let nextRank = currentRank + 1;
|
|
71
|
+
nextRank < vote.choice.length;
|
|
72
|
+
nextRank++
|
|
73
|
+
) {
|
|
74
|
+
const preferredChoice = vote.choice[currentRank] - 1;
|
|
75
|
+
const lowerChoice = vote.choice[nextRank] - 1;
|
|
76
|
+
pairwiseComparisons[preferredChoice][lowerChoice] += vote.balance;
|
|
77
|
+
pairwiseComparisons[lowerChoice][preferredChoice] -= vote.balance;
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
// Calculate Copeland scores
|
|
74
83
|
const scores = Array(choicesCount).fill(0);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
let totalCopelandScore = 0;
|
|
85
|
+
|
|
86
|
+
for (let choiceIndex = 0; choiceIndex < choicesCount; choiceIndex++) {
|
|
87
|
+
for (
|
|
88
|
+
let opponentIndex = 0;
|
|
89
|
+
opponentIndex < choicesCount;
|
|
90
|
+
opponentIndex++
|
|
91
|
+
) {
|
|
92
|
+
if (choiceIndex !== opponentIndex) {
|
|
93
|
+
const comparison = pairwiseComparisons[choiceIndex][opponentIndex];
|
|
94
|
+
if (comparison > 0) {
|
|
95
|
+
scores[choiceIndex]++;
|
|
96
|
+
} else if (comparison < 0) {
|
|
97
|
+
scores[opponentIndex]++;
|
|
82
98
|
} else {
|
|
83
|
-
scores[
|
|
84
|
-
scores[
|
|
99
|
+
scores[choiceIndex] += 0.5;
|
|
100
|
+
scores[opponentIndex] += 0.5;
|
|
85
101
|
}
|
|
86
102
|
}
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
|
|
90
|
-
|
|
106
|
+
// Calculate total Copeland score for normalization
|
|
107
|
+
totalCopelandScore = scores.reduce((sum, score) => sum + score, 0);
|
|
108
|
+
|
|
109
|
+
// Normalize scores to distribute voting power
|
|
110
|
+
if (totalCopelandScore > 0) {
|
|
111
|
+
return scores.map(
|
|
112
|
+
(score) => (score / totalCopelandScore) * totalVotingPower
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If no clear winners, distribute power equally
|
|
117
|
+
return scores.map(() => totalVotingPower / choicesCount);
|
|
91
118
|
}
|
|
92
119
|
|
|
93
120
|
// Calculates the Copeland scores for each choice, broken down by strategy
|
|
@@ -99,15 +126,37 @@ export default class CopelandVoting {
|
|
|
99
126
|
Array.from({ length: choicesCount }, () => Array(strategiesCount).fill(0))
|
|
100
127
|
);
|
|
101
128
|
|
|
129
|
+
// Calculate total voting power per strategy
|
|
130
|
+
const strategyTotals = Array(strategiesCount).fill(0);
|
|
131
|
+
for (const vote of validVotes) {
|
|
132
|
+
for (let i = 0; i < strategiesCount; i++) {
|
|
133
|
+
strategyTotals[i] += vote.scores[i];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
102
137
|
// Calculate pairwise comparisons for each strategy
|
|
103
138
|
for (const vote of validVotes) {
|
|
104
|
-
for (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
for (
|
|
140
|
+
let currentRank = 0;
|
|
141
|
+
currentRank < vote.choice.length;
|
|
142
|
+
currentRank++
|
|
143
|
+
) {
|
|
144
|
+
for (
|
|
145
|
+
let nextRank = currentRank + 1;
|
|
146
|
+
nextRank < vote.choice.length;
|
|
147
|
+
nextRank++
|
|
148
|
+
) {
|
|
149
|
+
const preferredChoice = vote.choice[currentRank] - 1;
|
|
150
|
+
const lowerChoice = vote.choice[nextRank] - 1;
|
|
151
|
+
for (
|
|
152
|
+
let strategyIndex = 0;
|
|
153
|
+
strategyIndex < strategiesCount;
|
|
154
|
+
strategyIndex++
|
|
155
|
+
) {
|
|
156
|
+
pairwiseComparisons[preferredChoice][lowerChoice][strategyIndex] +=
|
|
157
|
+
vote.scores[strategyIndex];
|
|
158
|
+
pairwiseComparisons[lowerChoice][preferredChoice][strategyIndex] -=
|
|
159
|
+
vote.scores[strategyIndex];
|
|
111
160
|
}
|
|
112
161
|
}
|
|
113
162
|
}
|
|
@@ -118,24 +167,66 @@ export default class CopelandVoting {
|
|
|
118
167
|
Array(strategiesCount).fill(0)
|
|
119
168
|
);
|
|
120
169
|
|
|
121
|
-
for (let
|
|
122
|
-
for (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
170
|
+
for (let choiceIndex = 0; choiceIndex < choicesCount; choiceIndex++) {
|
|
171
|
+
for (
|
|
172
|
+
let opponentIndex = 0;
|
|
173
|
+
opponentIndex < choicesCount;
|
|
174
|
+
opponentIndex++
|
|
175
|
+
) {
|
|
176
|
+
if (choiceIndex !== opponentIndex) {
|
|
177
|
+
for (
|
|
178
|
+
let strategyIndex = 0;
|
|
179
|
+
strategyIndex < strategiesCount;
|
|
180
|
+
strategyIndex++
|
|
181
|
+
) {
|
|
182
|
+
const comparison =
|
|
183
|
+
pairwiseComparisons[choiceIndex][opponentIndex][strategyIndex];
|
|
184
|
+
if (comparison > 0) {
|
|
185
|
+
scores[choiceIndex][strategyIndex]++;
|
|
186
|
+
} else if (comparison < 0) {
|
|
187
|
+
scores[opponentIndex][strategyIndex]++;
|
|
129
188
|
} else {
|
|
130
|
-
scores[
|
|
131
|
-
scores[
|
|
189
|
+
scores[choiceIndex][strategyIndex] += 0.5;
|
|
190
|
+
scores[opponentIndex][strategyIndex] += 0.5;
|
|
132
191
|
}
|
|
133
192
|
}
|
|
134
193
|
}
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
196
|
|
|
138
|
-
|
|
197
|
+
// Normalize scores by strategy to distribute voting power
|
|
198
|
+
const normalizedScores = Array.from({ length: choicesCount }, () =>
|
|
199
|
+
Array(strategiesCount).fill(0)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
for (
|
|
203
|
+
let strategyIndex = 0;
|
|
204
|
+
strategyIndex < strategiesCount;
|
|
205
|
+
strategyIndex++
|
|
206
|
+
) {
|
|
207
|
+
// Calculate total Copeland score for this strategy
|
|
208
|
+
let totalCopelandScore = 0;
|
|
209
|
+
for (let choiceIndex = 0; choiceIndex < choicesCount; choiceIndex++) {
|
|
210
|
+
totalCopelandScore += scores[choiceIndex][strategyIndex];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Normalize scores to distribute voting power for this strategy
|
|
214
|
+
if (totalCopelandScore > 0) {
|
|
215
|
+
for (let choiceIndex = 0; choiceIndex < choicesCount; choiceIndex++) {
|
|
216
|
+
normalizedScores[choiceIndex][strategyIndex] =
|
|
217
|
+
(scores[choiceIndex][strategyIndex] / totalCopelandScore) *
|
|
218
|
+
strategyTotals[strategyIndex];
|
|
219
|
+
}
|
|
220
|
+
} else if (strategyTotals[strategyIndex] > 0) {
|
|
221
|
+
// If no clear winners, distribute power equally for this strategy
|
|
222
|
+
for (let choiceIndex = 0; choiceIndex < choicesCount; choiceIndex++) {
|
|
223
|
+
normalizedScores[choiceIndex][strategyIndex] =
|
|
224
|
+
strategyTotals[strategyIndex] / choicesCount;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return normalizedScores;
|
|
139
230
|
}
|
|
140
231
|
|
|
141
232
|
// Calculates the total score (sum of all valid vote balances)
|