types.gno
6.35 Kb · 242 lines
1package impl
2
3import (
4 "strings"
5
6 "gno.land/p/nt/bptree/v0"
7 "gno.land/p/nt/markdown/sanitize/v0"
8 "gno.land/p/nt/ufmt/v0"
9 "gno.land/r/gov/dao"
10 "gno.land/r/gov/dao/memberstore/v0"
11)
12
13type Law struct {
14 Supermajority float64
15}
16
17func NewLaw(supermajority float64) Law {
18 return Law{Supermajority: supermajority}
19}
20
21func (l *Law) String() string {
22 return ufmt.Sprintf("This law contains the following data:\n\n- Supermajority: %v%%", l.Supermajority)
23}
24
25// ProposalsStatuses contains the status of all the proposals indexed by the proposal ID.
26type ProposalsStatuses struct {
27 *bptree.BPTree // map[int]*proposalStatus
28}
29
30func NewProposalsStatuses() ProposalsStatuses {
31 return ProposalsStatuses{bptree.NewBPTree32()}
32}
33
34func (pss ProposalsStatuses) GetStatus(id dao.ProposalID) *proposalStatus {
35 if pss.BPTree == nil {
36 return nil
37 }
38
39 pids := id.String()
40 psv := pss.Get(pids)
41 if psv == nil {
42 return nil
43 }
44
45 ps, ok := psv.(*proposalStatus)
46 if !ok {
47 panic("ProposalsStatuses must contains only proposalStatus types")
48 }
49
50 return ps
51}
52
53type proposalStatus struct {
54 YesVotes memberstore.MembersByTier
55 NoVotes memberstore.MembersByTier
56 AbstainVotes memberstore.MembersByTier
57 AllVotes memberstore.MembersByTier
58
59 Accepted bool
60 Denied bool
61
62 DeniedReason string
63
64 TiersAllowedToVote []string
65}
66
67func getMembers(cur realm) memberstore.MembersByTier {
68 return memberstore.Get(0, cur)
69}
70
71func newEmptyVoteStore() memberstore.MembersByTier {
72 mbt := memberstore.NewMembersByTier()
73 mbt.SetTier(memberstore.T1)
74 mbt.SetTier(memberstore.T2)
75 mbt.SetTier(memberstore.T3)
76 return mbt
77}
78
79func newProposalStatus(allowedToVote []string) *proposalStatus {
80 return &proposalStatus{
81 YesVotes: newEmptyVoteStore(),
82 NoVotes: newEmptyVoteStore(),
83 AbstainVotes: newEmptyVoteStore(),
84 AllVotes: newEmptyVoteStore(),
85 TiersAllowedToVote: allowedToVote,
86 }
87}
88
89// totalPower computes the total voting power dynamically from current members
90// rather than using a snapshot. See https://github.com/gnolang/gno/pull/5271#discussion_r2952523023
91//
92// Non-crossing: rlm is threaded into the crossing getMembers(cross(rlm))
93// call. The proposalStatus methods that compose this (votePowerPercent,
94// YesPercent etc., String) all take `_ int, rlm realm` for the same reason.
95func (ps *proposalStatus) totalPower(_ int, rlm realm) float64 {
96 members := getMembers(cross(rlm))
97 var tp float64
98 for _, tn := range ps.TiersAllowedToVote {
99 power := memberstore.GetTierPower(tn, members)
100 tp += power * float64(members.GetTierSize(tn))
101 }
102 return tp
103}
104
105func (ps *proposalStatus) votePowerPercent(_ int, rlm realm, votes memberstore.MembersByTier) float64 {
106 members := getMembers(cross(rlm))
107 var vp float64
108 memberstore.IterateTiers(func(tn string, tier memberstore.Tier) bool {
109 power := memberstore.GetTierPower(tn, members)
110 ts := votes.GetTierSize(tn)
111 vp = vp + (power * float64(ts))
112 return false
113 })
114 tp := ps.totalPower(0, rlm)
115 if tp == 0 {
116 return 0
117 }
118 return (vp / tp) * 100
119}
120
121func (ps *proposalStatus) YesPercent(_ int, rlm realm) float64 {
122 return ps.votePowerPercent(0, rlm, ps.YesVotes)
123}
124
125func (ps *proposalStatus) NoPercent(_ int, rlm realm) float64 {
126 return ps.votePowerPercent(0, rlm, ps.NoVotes)
127}
128
129func (ps *proposalStatus) AbstainPercent(_ int, rlm realm) float64 {
130 return ps.votePowerPercent(0, rlm, ps.AbstainVotes)
131}
132
133func (ps *proposalStatus) IsAllowed(tier string) bool {
134 for _, ta := range ps.TiersAllowedToVote {
135 if ta == tier {
136 return true
137 }
138 }
139
140 return false
141}
142
143// XXX: can be optimized by passing down total power to Yes/No/Abstain percent fn to avoid re-computing
144func (ps *proposalStatus) String(_ int, rlm realm) string {
145 var sb strings.Builder
146 sb.WriteString("### Stats\n")
147
148 if ps.Accepted {
149 sb.WriteString("- **PROPOSAL HAS BEEN ACCEPTED**\n")
150 } else if ps.Denied {
151 sb.WriteString("- **PROPOSAL HAS BEEN DENIED**\n")
152 if ps.DeniedReason != "" {
153 sb.WriteString("REASON: ")
154 // DeniedReason is "execution failed: " + err.Error() from the
155 // proposal's executor callback, which any third-party realm can
156 // supply. Raw, it wrote attacker-controlled markdown directly beneath
157 // the "PROPOSAL HAS BEEN DENIED" line. InlineText folds newlines, so
158 // the reason cannot leave the REASON: line, and escapes emphasis and
159 // raw HTML.
160 sb.WriteString(sanitize.InlineText(clampField(ps.DeniedReason, maxRenderedReason)))
161 sb.WriteString("\n")
162 }
163 } else {
164 sb.WriteString("- **Proposal is open for votes**\n")
165 }
166
167 sb.WriteString("- Tiers eligible to vote: ")
168 sb.WriteString(strings.Join(ps.TiersAllowedToVote, ", "))
169 sb.WriteString("\n")
170
171 sb.WriteString(ufmt.Sprintf("- YES PERCENT: %v%%\n", ps.YesPercent(0, rlm)))
172 sb.WriteString(ufmt.Sprintf("- NO PERCENT: %v%%\n", ps.NoPercent(0, rlm)))
173 sb.WriteString(ufmt.Sprintf("- ABSTAIN PERCENT: %v%%\n", ps.AbstainPercent(0, rlm)))
174
175 return sb.String()
176}
177
178func StringifyVotes(_ int, rlm realm, ps *proposalStatus) string {
179 var sb strings.Builder
180
181 writeVotes(0, rlm, &sb, ps.YesVotes, "YES")
182 writeVotes(0, rlm, &sb, ps.NoVotes, "NO")
183 writeVotes(0, rlm, &sb, ps.AbstainVotes, "ABSTAIN")
184
185 if sb.String() == "" {
186 return "No one voted yet."
187 }
188
189 return sb.String()
190}
191
192func writeVotes(_ int, rlm realm, sb *strings.Builder, t memberstore.MembersByTier, title string) {
193 if t.Size() == 0 {
194 return
195 }
196 members := getMembers(cross(rlm))
197 t.Iterate("", "", func(tn string, value interface{}) bool {
198 _, ok := memberstore.GetTier(tn)
199 if !ok {
200 panic("tier not found")
201 }
202
203 power := memberstore.GetTierPower(tn, members)
204
205 sb.WriteString(ufmt.Sprintf("%v from %v (VPPM %v):\n\n", title, tn, power))
206 ms, _ := value.(*bptree.BPTree)
207 ms.Iterate("", "", func(addr string, _ interface{}) bool {
208 sb.WriteString("- " + tryResolveAddr(address(addr)) + "\n")
209 return false
210 })
211
212 sb.WriteString("\n")
213
214 return false
215 })
216}
217
218func StringifyProposal(p *dao.Proposal) string {
219 out := ufmt.Sprintf(`
220### Title: %s
221
222### Proposed by: %s
223
224%s
225`, p.Title(), p.Author(), p.Description())
226
227 if p.ExecutorString() != "" {
228 out += ufmt.Sprintf(`
229This proposal contains the following metadata:
230
231%s
232`, p.ExecutorString())
233 }
234
235 // Disclosed independently of the metadata string, and escaped, for the
236 // same reasons as render.renderProposalPage.
237 if cr := sanitize.InlineCode(strings.TrimSpace(clampField(p.ExecutorCreationRealm(), maxRenderedRealm))); cr != "" {
238 out += ufmt.Sprintf("\nExecutor created in: %s\n", cr)
239 }
240
241 return out
242}