@snapshot-labs/snapshot.js 0.12.49 → 0.12.51

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.
@@ -831,6 +831,7 @@ declare const _default: {
831
831
  approval: typeof import("./voting/approval").default;
832
832
  quadratic: typeof import("./voting/quadratic").default;
833
833
  'ranked-choice': typeof import("./voting/rankedChoice").default;
834
+ copeland: typeof import("./voting/copeland").default;
834
835
  weighted: typeof import("./voting/weighted").default;
835
836
  basic: typeof import("./voting/singleChoice").default;
836
837
  };
@@ -63,6 +63,7 @@ declare const _default: {
63
63
  approval: typeof import("./voting/approval").default;
64
64
  quadratic: typeof import("./voting/quadratic").default;
65
65
  'ranked-choice': typeof import("./voting/rankedChoice").default;
66
+ copeland: typeof import("./voting/copeland").default;
66
67
  weighted: typeof import("./voting/weighted").default;
67
68
  basic: typeof import("./voting/singleChoice").default;
68
69
  };
@@ -0,0 +1,18 @@
1
+ import { Strategy, RankedChoiceVote } from './types';
2
+ export default class CopelandVoting {
3
+ proposal: {
4
+ choices: string[];
5
+ };
6
+ votes: RankedChoiceVote[];
7
+ strategies: Strategy[];
8
+ selected: number[];
9
+ constructor(proposal: {
10
+ choices: string[];
11
+ }, votes: RankedChoiceVote[], strategies: Strategy[], selected: number[]);
12
+ static isValidChoice(voteChoice: number[], proposalChoices: string[]): boolean;
13
+ getValidVotes(): RankedChoiceVote[];
14
+ getScores(): number[];
15
+ getScoresByStrategy(): number[][];
16
+ getScoresTotal(): number;
17
+ getChoiceString(): string;
18
+ }
@@ -2,12 +2,14 @@ import singleChoice from './singleChoice';
2
2
  import approval from './approval';
3
3
  import quadratic from './quadratic';
4
4
  import rankedChoice from './rankedChoice';
5
+ import copeland from './copeland';
5
6
  import weighted from './weighted';
6
7
  declare const _default: {
7
8
  'single-choice': typeof singleChoice;
8
9
  approval: typeof approval;
9
10
  quadratic: typeof quadratic;
10
11
  'ranked-choice': typeof rankedChoice;
12
+ copeland: typeof copeland;
11
13
  weighted: typeof weighted;
12
14
  basic: typeof singleChoice;
13
15
  };
@@ -1,4 +1,4 @@
1
- import { QuadraticVote, QuadraticChoice, Strategy } from './types';
1
+ import { QuadraticVote, ChoiceMap, Strategy } from './types';
2
2
  export declare function calcPercentageOfSum(part: number, wholeArray: number[]): number;
3
3
  export declare function calcSqrt(percentageWeight: number, votingPower: number): number;
4
4
  export default class QuadraticVoting {
@@ -7,11 +7,11 @@ export default class QuadraticVoting {
7
7
  };
8
8
  votes: QuadraticVote[];
9
9
  strategies: Strategy[];
10
- selected: QuadraticChoice;
10
+ selected: ChoiceMap;
11
11
  constructor(proposal: {
12
12
  choices: string[];
13
- }, votes: QuadraticVote[], strategies: Strategy[], selected: QuadraticChoice);
14
- static isValidChoice(voteChoice: QuadraticChoice, proposalChoices: string[]): boolean;
13
+ }, votes: QuadraticVote[], strategies: Strategy[], selected: ChoiceMap);
14
+ static isValidChoice(voteChoice: ChoiceMap, proposalChoices: string[]): boolean;
15
15
  getValidVotes(): QuadraticVote[];
16
16
  getScores(): number[];
17
17
  getScoresByStrategy(): number[][];
@@ -3,33 +3,17 @@ export interface Strategy {
3
3
  network: string;
4
4
  params: Record<string, unknown>;
5
5
  }
6
- export interface SingleChoiceVote {
7
- choice: number;
6
+ interface BaseVote<TChoice> {
7
+ choice: TChoice;
8
8
  balance: number;
9
9
  scores: number[];
10
10
  }
11
- export interface ApprovalVote {
12
- choice: number[];
13
- balance: number;
14
- scores: number[];
15
- }
16
- export interface RankedChoiceVote {
17
- choice: number[];
18
- balance: number;
19
- scores: number[];
20
- }
21
- export interface QuadraticChoice {
11
+ export type ChoiceMap = {
22
12
  [key: string]: number;
23
- }
24
- export interface QuadraticVote {
25
- choice: QuadraticChoice;
26
- balance: number;
27
- scores: number[];
28
- }
29
- export interface WeightedVote {
30
- choice: {
31
- [key: string]: number;
32
- };
33
- balance: number;
34
- scores: number[];
35
- }
13
+ };
14
+ export type SingleChoiceVote = BaseVote<number>;
15
+ export type ApprovalVote = BaseVote<number[]>;
16
+ export type RankedChoiceVote = BaseVote<number[]>;
17
+ export type QuadraticVote = BaseVote<ChoiceMap>;
18
+ export type WeightedVote = BaseVote<ChoiceMap>;
19
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapshot-labs/snapshot.js",
3
- "version": "0.12.49",
3
+ "version": "0.12.51",
4
4
  "repository": "snapshot-labs/snapshot.js",
5
5
  "license": "MIT",
6
6
  "main": "dist/snapshot.cjs.js",
@@ -47,6 +47,7 @@
47
47
  "approval",
48
48
  "ranked-choice",
49
49
  "quadratic",
50
+ "copeland",
50
51
  "weighted",
51
52
  "custom",
52
53
  "basic"
package/src/sign/index.ts CHANGED
@@ -124,6 +124,7 @@ export default class Client {
124
124
  address: string,
125
125
  message: Proposal
126
126
  ) {
127
+ if (!message.labels) message.labels = [];
127
128
  if (!message.discussion) message.discussion = '';
128
129
  if (!message.app) message.app = '';
129
130
  if (!message.privacy) message.privacy = '';
@@ -0,0 +1,134 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Partial ranking 1`] = `
4
+ [
5
+ 4,
6
+ 6,
7
+ 2,
8
+ 0,
9
+ ]
10
+ `;
11
+
12
+ exports[`getScores 1`] = `
13
+ [
14
+ 5,
15
+ 5,
16
+ 2,
17
+ 0,
18
+ ]
19
+ `;
20
+
21
+ exports[`getScores 2`] = `
22
+ [
23
+ 5,
24
+ 5,
25
+ 2,
26
+ 0,
27
+ ]
28
+ `;
29
+
30
+ exports[`getScores 3`] = `
31
+ [
32
+ 5,
33
+ 5,
34
+ 2,
35
+ 0,
36
+ ]
37
+ `;
38
+
39
+ exports[`getScores 4`] = `
40
+ [
41
+ 5,
42
+ 5,
43
+ 2,
44
+ 0,
45
+ ]
46
+ `;
47
+
48
+ exports[`getScoresByStrategy 1`] = `
49
+ [
50
+ [
51
+ 5,
52
+ ],
53
+ [
54
+ 5,
55
+ ],
56
+ [
57
+ 2,
58
+ ],
59
+ [
60
+ 0,
61
+ ],
62
+ ]
63
+ `;
64
+
65
+ exports[`getScoresByStrategy 2`] = `
66
+ [
67
+ [
68
+ 5,
69
+ ],
70
+ [
71
+ 5,
72
+ ],
73
+ [
74
+ 2,
75
+ ],
76
+ [
77
+ 0,
78
+ ],
79
+ ]
80
+ `;
81
+
82
+ exports[`getScoresByStrategy 3`] = `
83
+ [
84
+ [
85
+ 5,
86
+ 5,
87
+ 5,
88
+ ],
89
+ [
90
+ 5,
91
+ 5,
92
+ 5,
93
+ ],
94
+ [
95
+ 2,
96
+ 2,
97
+ 2,
98
+ ],
99
+ [
100
+ 0,
101
+ 0,
102
+ 0,
103
+ ],
104
+ ]
105
+ `;
106
+
107
+ exports[`getScoresByStrategy 4`] = `
108
+ [
109
+ [
110
+ 5,
111
+ 5,
112
+ 5,
113
+ ],
114
+ [
115
+ 5,
116
+ 5,
117
+ 5,
118
+ ],
119
+ [
120
+ 2,
121
+ 2,
122
+ 2,
123
+ ],
124
+ [
125
+ 0,
126
+ 0,
127
+ 0,
128
+ ],
129
+ ]
130
+ `;
131
+
132
+ exports[`getScoresTotal 1`] = `4`;
133
+
134
+ exports[`getScoresTotal 2`] = `12`;
@@ -0,0 +1,129 @@
1
+ import { test, expect } from 'vitest';
2
+ import CopelandVoting from './copeland';
3
+ import example from './examples/copeland.json';
4
+
5
+ // Helper function to create a more complex example with multiple strategies
6
+ const example2 = () => {
7
+ const proposal = {
8
+ choices: ['Alice', 'Bob', 'Carol', 'David']
9
+ };
10
+ const strategies = [
11
+ { name: 'ticket', network: '1', params: {} },
12
+ { name: 'ticket', network: '1', params: {} },
13
+ { name: 'ticket', network: '1', params: {} }
14
+ ];
15
+ const votes = example.votes.map((vote) => ({
16
+ choice: vote.choice,
17
+ balance: 3,
18
+ scores: [1, 1, 1]
19
+ }));
20
+
21
+ return {
22
+ proposal,
23
+ strategies,
24
+ votes
25
+ };
26
+ };
27
+
28
+ // Generate a set of votes including some invalid choices
29
+ const votesWithInvalidChoices = () => {
30
+ const invalidVotes = [
31
+ { choice: [0, 1], balance: 1, scores: [1] },
32
+ { choice: [1, 5], balance: 1, scores: [1] },
33
+ { choice: [1, 1], balance: 1, scores: [1] },
34
+ { choice: [], balance: 1, scores: [1] },
35
+ { choice: [1, 2, 3, 4, 5], balance: 1, scores: [1] }
36
+ ];
37
+ return [...invalidVotes, ...example.votes];
38
+ };
39
+
40
+ // Generate a set of votes including some invalid choices for the multi-strategy example
41
+ const votesWithInvalidChoices2 = () => {
42
+ const invalidVotes = [
43
+ { choice: [0, 1], balance: 3, scores: [1, 1, 1] },
44
+ { choice: [1, 5], balance: 3, scores: [1, 1, 1] },
45
+ { choice: [1, 1], balance: 3, scores: [1, 1, 1] },
46
+ { choice: [], balance: 3, scores: [1, 1, 1] },
47
+ { choice: [1, 2, 3, 4, 5], balance: 3, scores: [1, 1, 1] }
48
+ ];
49
+ return [...invalidVotes, ...example2().votes];
50
+ };
51
+
52
+ // Test cases for getScores method
53
+ test.each([
54
+ [example.proposal, example.votes, example.strategies],
55
+ [example.proposal, votesWithInvalidChoices(), example.strategies],
56
+ [example2().proposal, example2().votes, example2().strategies],
57
+ [example2().proposal, votesWithInvalidChoices2(), example2().strategies]
58
+ ])('getScores', (proposal, votes, strategies) => {
59
+ const copeland = new CopelandVoting(
60
+ proposal,
61
+ votes,
62
+ strategies,
63
+ example.selectedChoice
64
+ );
65
+ expect(copeland.getScores()).toMatchSnapshot();
66
+ });
67
+
68
+ // Test cases for getScoresByStrategy method
69
+ test.each([
70
+ [example.proposal, example.votes, example.strategies],
71
+ [example.proposal, votesWithInvalidChoices(), example.strategies],
72
+ [example2().proposal, example2().votes, example2().strategies],
73
+ [example2().proposal, votesWithInvalidChoices2(), example2().strategies]
74
+ ])('getScoresByStrategy', (proposal, votes, strategies) => {
75
+ const copeland = new CopelandVoting(
76
+ proposal,
77
+ votes,
78
+ strategies,
79
+ example.selectedChoice
80
+ );
81
+ expect(copeland.getScoresByStrategy()).toMatchSnapshot();
82
+ });
83
+
84
+ // Test cases for getScoresTotal method
85
+ test.each([
86
+ [example.proposal, example.votes, example.strategies],
87
+ [example2().proposal, example2().votes, example2().strategies]
88
+ ])('getScoresTotal', (proposal, votes, strategies) => {
89
+ const copeland = new CopelandVoting(
90
+ proposal,
91
+ votes,
92
+ strategies,
93
+ example.selectedChoice
94
+ );
95
+ expect(copeland.getScoresTotal()).toMatchSnapshot();
96
+ });
97
+
98
+ // Test cases for getChoiceString method
99
+ test.each([
100
+ [[1, 2], 'Alice, Bob'],
101
+ [[4, 2, 3, 1], 'David, Bob, Carol, Alice']
102
+ ])('getChoiceString %s', (selected, expected) => {
103
+ const copeland = new CopelandVoting(
104
+ example.proposal,
105
+ example.votes,
106
+ example.strategies,
107
+ selected
108
+ );
109
+ expect(copeland.getChoiceString()).toBe(expected);
110
+ });
111
+
112
+ // Test case for partial ranking
113
+ test('Partial ranking', () => {
114
+ const partialVotes = [
115
+ ...example.votes,
116
+ {
117
+ choice: [2, 1],
118
+ balance: 1,
119
+ scores: [1]
120
+ }
121
+ ];
122
+ const copeland = new CopelandVoting(
123
+ example.proposal,
124
+ partialVotes,
125
+ example.strategies,
126
+ example.selectedChoice
127
+ );
128
+ expect(copeland.getScores()).toMatchSnapshot();
129
+ });
@@ -0,0 +1,155 @@
1
+ import { Strategy, RankedChoiceVote } from './types';
2
+
3
+ // CopelandVoting implements ranked choice voting using Copeland's method
4
+ // This method compares each pair of choices and awards points based on pairwise victories
5
+ export default class CopelandVoting {
6
+ proposal: { choices: string[] };
7
+ votes: RankedChoiceVote[];
8
+ strategies: Strategy[];
9
+ selected: number[];
10
+
11
+ constructor(
12
+ proposal: { choices: string[] },
13
+ votes: RankedChoiceVote[],
14
+ strategies: Strategy[],
15
+ selected: number[]
16
+ ) {
17
+ this.proposal = proposal;
18
+ this.votes = votes;
19
+ this.strategies = strategies;
20
+ this.selected = selected;
21
+ }
22
+
23
+ // Validates if a vote choice is valid for the given proposal
24
+ // Allows partial ranking (not all choices need to be ranked)
25
+ static isValidChoice(
26
+ voteChoice: number[],
27
+ proposalChoices: string[]
28
+ ): boolean {
29
+ if (
30
+ !Array.isArray(voteChoice) ||
31
+ voteChoice.length === 0 ||
32
+ voteChoice.length > proposalChoices.length ||
33
+ new Set(voteChoice).size !== voteChoice.length
34
+ ) {
35
+ return false;
36
+ }
37
+
38
+ return voteChoice.every(
39
+ (choice) =>
40
+ Number.isInteger(choice) &&
41
+ choice >= 1 &&
42
+ choice <= proposalChoices.length
43
+ );
44
+ }
45
+
46
+ // Returns only the valid votes
47
+ getValidVotes(): RankedChoiceVote[] {
48
+ return this.votes.filter((vote) =>
49
+ CopelandVoting.isValidChoice(vote.choice, this.proposal.choices)
50
+ );
51
+ }
52
+
53
+ // Calculates the Copeland scores for each choice
54
+ getScores(): number[] {
55
+ const validVotes = this.getValidVotes();
56
+ const choicesCount = this.proposal.choices.length;
57
+ const pairwiseComparisons = Array.from({ length: choicesCount }, () =>
58
+ Array(choicesCount).fill(0)
59
+ );
60
+
61
+ // Calculate pairwise comparisons
62
+ for (const vote of validVotes) {
63
+ for (let i = 0; i < vote.choice.length; i++) {
64
+ for (let j = i + 1; j < vote.choice.length; j++) {
65
+ const winner = vote.choice[i] - 1;
66
+ const loser = vote.choice[j] - 1;
67
+ pairwiseComparisons[winner][loser] += vote.balance;
68
+ pairwiseComparisons[loser][winner] -= vote.balance;
69
+ }
70
+ }
71
+ }
72
+
73
+ // Calculate Copeland scores
74
+ const scores = Array(choicesCount).fill(0);
75
+ for (let i = 0; i < choicesCount; i++) {
76
+ for (let j = 0; j < choicesCount; j++) {
77
+ if (i !== j) {
78
+ if (pairwiseComparisons[i][j] > 0) {
79
+ scores[i]++;
80
+ } else if (pairwiseComparisons[i][j] < 0) {
81
+ scores[j]++;
82
+ } else {
83
+ scores[i] += 0.5;
84
+ scores[j] += 0.5;
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ return scores;
91
+ }
92
+
93
+ // Calculates the Copeland scores for each choice, broken down by strategy
94
+ getScoresByStrategy(): number[][] {
95
+ const validVotes = this.getValidVotes();
96
+ const choicesCount = this.proposal.choices.length;
97
+ const strategiesCount = this.strategies.length;
98
+ const pairwiseComparisons = Array.from({ length: choicesCount }, () =>
99
+ Array.from({ length: choicesCount }, () => Array(strategiesCount).fill(0))
100
+ );
101
+
102
+ // Calculate pairwise comparisons for each strategy
103
+ for (const vote of validVotes) {
104
+ for (let i = 0; i < vote.choice.length; i++) {
105
+ for (let j = i + 1; j < vote.choice.length; j++) {
106
+ const winner = vote.choice[i] - 1;
107
+ const loser = vote.choice[j] - 1;
108
+ for (let s = 0; s < strategiesCount; s++) {
109
+ pairwiseComparisons[winner][loser][s] += vote.scores[s];
110
+ pairwiseComparisons[loser][winner][s] -= vote.scores[s];
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // Calculate Copeland scores for each strategy
117
+ const scores = Array.from({ length: choicesCount }, () =>
118
+ Array(strategiesCount).fill(0)
119
+ );
120
+
121
+ for (let i = 0; i < choicesCount; i++) {
122
+ for (let j = 0; j < choicesCount; j++) {
123
+ if (i !== j) {
124
+ for (let s = 0; s < strategiesCount; s++) {
125
+ if (pairwiseComparisons[i][j][s] > 0) {
126
+ scores[i][s]++;
127
+ } else if (pairwiseComparisons[i][j][s] < 0) {
128
+ scores[j][s]++;
129
+ } else {
130
+ scores[i][s] += 0.5;
131
+ scores[j][s] += 0.5;
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ return scores;
139
+ }
140
+
141
+ // Calculates the total score (sum of all valid vote balances)
142
+ getScoresTotal(): number {
143
+ return this.getValidVotes().reduce(
144
+ (total, vote) => total + vote.balance,
145
+ 0
146
+ );
147
+ }
148
+
149
+ // Returns a string representation of the selected choices
150
+ getChoiceString(): string {
151
+ return this.selected
152
+ .map((choice) => this.proposal.choices[choice - 1])
153
+ .join(', ');
154
+ }
155
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "proposal": {
3
+ "choices": ["Alice", "Bob", "Carol", "David"]
4
+ },
5
+ "strategies": [{ "name": "ticket", "network": "1", "params": {} }],
6
+ "scores": [2.5, 2, 0.5, 1],
7
+ "scoresByStrategy": [[2.5], [2], [0.5], [1]],
8
+ "scoresTotal": 4,
9
+ "selectedChoice": [1, 2],
10
+ "votes": [
11
+ {
12
+ "choice": [1, 2, 3, 4],
13
+ "balance": 1,
14
+ "scores": [1]
15
+ },
16
+ {
17
+ "choice": [2, 1, 3, 4],
18
+ "balance": 1,
19
+ "scores": [1]
20
+ },
21
+ {
22
+ "choice": [1, 3, 2, 4],
23
+ "balance": 1,
24
+ "scores": [1]
25
+ },
26
+ {
27
+ "choice": [2, 1],
28
+ "balance": 1,
29
+ "scores": [1]
30
+ }
31
+ ]
32
+ }
@@ -2,6 +2,7 @@ import singleChoice from './singleChoice';
2
2
  import approval from './approval';
3
3
  import quadratic from './quadratic';
4
4
  import rankedChoice from './rankedChoice';
5
+ import copeland from './copeland';
5
6
  import weighted from './weighted';
6
7
 
7
8
  export default {
@@ -9,6 +10,7 @@ export default {
9
10
  approval,
10
11
  quadratic,
11
12
  'ranked-choice': rankedChoice,
13
+ copeland,
12
14
  weighted,
13
15
  basic: singleChoice
14
16
  };
@@ -1,4 +1,4 @@
1
- import { QuadraticVote, QuadraticChoice, Strategy } from './types';
1
+ import { QuadraticVote, ChoiceMap, Strategy } from './types';
2
2
 
3
3
  export function calcPercentageOfSum(
4
4
  part: number,
@@ -34,13 +34,13 @@ export default class QuadraticVoting {
34
34
  proposal: { choices: string[] };
35
35
  votes: QuadraticVote[];
36
36
  strategies: Strategy[];
37
- selected: QuadraticChoice;
37
+ selected: ChoiceMap;
38
38
 
39
39
  constructor(
40
40
  proposal: { choices: string[] },
41
41
  votes: QuadraticVote[],
42
42
  strategies: Strategy[],
43
- selected: QuadraticChoice
43
+ selected: ChoiceMap
44
44
  ) {
45
45
  this.proposal = proposal;
46
46
  this.votes = votes;
@@ -49,7 +49,7 @@ export default class QuadraticVoting {
49
49
  }
50
50
 
51
51
  static isValidChoice(
52
- voteChoice: QuadraticChoice,
52
+ voteChoice: ChoiceMap,
53
53
  proposalChoices: string[]
54
54
  ): boolean {
55
55
  return (
@@ -4,36 +4,15 @@ export interface Strategy {
4
4
  params: Record<string, unknown>;
5
5
  }
6
6
 
7
- export interface SingleChoiceVote {
8
- choice: number;
7
+ interface BaseVote<TChoice> {
8
+ choice: TChoice;
9
9
  balance: number;
10
10
  scores: number[];
11
11
  }
12
12
 
13
- export interface ApprovalVote {
14
- choice: number[];
15
- balance: number;
16
- scores: number[];
17
- }
18
-
19
- export interface RankedChoiceVote {
20
- choice: number[];
21
- balance: number;
22
- scores: number[];
23
- }
24
-
25
- export interface QuadraticChoice {
26
- [key: string]: number;
27
- }
28
-
29
- export interface QuadraticVote {
30
- choice: QuadraticChoice;
31
- balance: number;
32
- scores: number[];
33
- }
34
-
35
- export interface WeightedVote {
36
- choice: { [key: string]: number };
37
- balance: number;
38
- scores: number[];
39
- }
13
+ export type ChoiceMap = { [key: string]: number };
14
+ export type SingleChoiceVote = BaseVote<number>;
15
+ export type ApprovalVote = BaseVote<number[]>;
16
+ export type RankedChoiceVote = BaseVote<number[]>;
17
+ export type QuadraticVote = BaseVote<ChoiceMap>;
18
+ export type WeightedVote = BaseVote<ChoiceMap>;