Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}