Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}