record.gno
5.45 Kb ยท 194 lines
1package commondao
2
3import (
4 "errors"
5 "math"
6
7 "gno.land/p/nt/avl"
8)
9
10// ErrVoteExists indicates that a user already voted.
11var ErrVoteExists = errors.New("user already voted")
12
13type (
14 // VoteIterFn defines a callback to iterate votes.
15 VoteIterFn func(Vote) (stop bool)
16
17 // VotesCountIterFn defines a callback to iterate voted choices.
18 VotesCountIterFn func(_ VoteChoice, voteCount int) (stop bool)
19
20 // Vote defines a single vote.
21 Vote struct {
22 // Address is the address of the user that this vote belons to.
23 Address address
24
25 // Choice contains the voted choice.
26 Choice VoteChoice
27
28 // Reason contains an optional reason for the vote.
29 Reason string
30
31 // Context can store any custom voting values related to the vote.
32 //
33 // Warning: When using context be careful if references/pointers are
34 // assigned to it because they could potentially be accessed anywhere,
35 // which could lead to unwanted indirect modifications.
36 Context any
37 }
38)
39
40// ReadonlyVotingRecord defines an read only voting record.
41type ReadonlyVotingRecord struct {
42 votes avl.Tree // string(address) -> Vote
43 count avl.Tree // string(choice) -> int
44}
45
46// Size returns the total number of votes that record contains.
47func (r ReadonlyVotingRecord) Size() int {
48 return r.votes.Size()
49}
50
51// Iterate iterates voting record votes.
52func (r ReadonlyVotingRecord) Iterate(offset, count int, reverse bool, fn VoteIterFn) bool {
53 cb := func(_ string, v any) bool { return fn(v.(Vote)) }
54 if reverse {
55 return r.votes.ReverseIterateByOffset(offset, count, cb)
56 }
57 return r.votes.IterateByOffset(offset, count, cb)
58}
59
60// IterateVotesCount iterates voted choices with the amount of votes submited for each.
61func (r ReadonlyVotingRecord) IterateVotesCount(fn VotesCountIterFn) bool {
62 return r.count.Iterate("", "", func(k string, v any) bool {
63 return fn(VoteChoice(k), v.(int))
64 })
65}
66
67// VoteCount returns the number of votes for a single voting choice.
68func (r ReadonlyVotingRecord) VoteCount(c VoteChoice) int {
69 if v, found := r.count.Get(string(c)); found {
70 return v.(int)
71 }
72 return 0
73}
74
75// HasVoted checks if an account already voted.
76func (r ReadonlyVotingRecord) HasVoted(user address) bool {
77 return r.votes.Has(user.String())
78}
79
80// GetVote returns a vote.
81func (r ReadonlyVotingRecord) GetVote(user address) (_ Vote, found bool) {
82 if v, found := r.votes.Get(user.String()); found {
83 return v.(Vote), true
84 }
85 return Vote{}, false
86}
87
88// VotingRecord stores accounts that voted and vote choices.
89type VotingRecord struct {
90 ReadonlyVotingRecord
91}
92
93// Readonly returns a read only voting record.
94func (r VotingRecord) Readonly() ReadonlyVotingRecord {
95 return r.ReadonlyVotingRecord
96}
97
98// AddVote adds a vote to the voting record.
99// If a vote for the same user already exists is overwritten.
100func (r *VotingRecord) AddVote(vote Vote) (updated bool) {
101 // Get previous member vote if it exists
102 v, _ := r.votes.Get(vote.Address.String())
103
104 // When a previous vote exists update counter for the previous choice
105 updated = r.votes.Set(vote.Address.String(), vote)
106 if updated {
107 prev := v.(Vote)
108 r.count.Set(string(prev.Choice), r.VoteCount(prev.Choice)-1)
109 }
110
111 r.count.Set(string(vote.Choice), r.VoteCount(vote.Choice)+1)
112 return
113}
114
115// FindMostVotedChoice returns the most voted choice.
116// ChoiceNone is returned when there is a tie between different
117// voting choices or when the voting record has are no votes.
118func FindMostVotedChoice(r ReadonlyVotingRecord) VoteChoice {
119 var (
120 choice VoteChoice
121 currentCount, prevCount int
122 )
123
124 r.IterateVotesCount(func(c VoteChoice, count int) bool {
125 if currentCount <= count {
126 choice = c
127 prevCount = currentCount
128 currentCount = count
129 }
130 return false
131 })
132
133 if prevCount < currentCount {
134 return choice
135 }
136 return ChoiceNone
137}
138
139// SelectChoiceByAbsoluteMajority select the vote choice by absolute majority.
140// Vote choice is a majority when chosen by more than half of the votes.
141// Absolute majority considers abstentions when counting votes.
142func SelectChoiceByAbsoluteMajority(r ReadonlyVotingRecord, membersCount int) (VoteChoice, bool) {
143 choice := FindMostVotedChoice(r)
144 if choice != ChoiceNone && r.VoteCount(choice) > int(membersCount/2) {
145 return choice, true
146 }
147 return ChoiceNone, false
148}
149
150// SelectChoiceBySuperMajority select the vote choice by super majority using a 2/3s threshold.
151// Abstentions are considered when calculating the super majority choice.
152func SelectChoiceBySuperMajority(r ReadonlyVotingRecord, membersCount int) (VoteChoice, bool) {
153 if membersCount < 3 {
154 return ChoiceNone, false
155 }
156
157 choice := FindMostVotedChoice(r)
158 if choice != ChoiceNone && r.VoteCount(choice) >= int(math.Ceil((2*float64(membersCount))/3)) {
159 return choice, true
160 }
161 return ChoiceNone, false
162}
163
164// SelectChoiceByPlurality selects the vote choice by plurality.
165// The choice will be considered a majority if it has votes and if there is no other
166// choice with the same number of votes. A tie won't be considered majority.
167func SelectChoiceByPlurality(r ReadonlyVotingRecord) (VoteChoice, bool) {
168 var (
169 choice VoteChoice
170 currentCount int
171 isMajority bool
172 )
173
174 r.IterateVotesCount(func(c VoteChoice, count int) bool {
175 // Don't consider explicit abstentions or invalid votes
176 if c == ChoiceAbstain || c == ChoiceNone {
177 return false
178 }
179
180 if currentCount < count {
181 choice = c
182 currentCount = count
183 isMajority = true
184 } else if currentCount == count {
185 isMajority = false
186 }
187 return false
188 })
189
190 if isMajority {
191 return choice, true
192 }
193 return ChoiceNone, false
194}