Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

5.46 Kb · 206 lines
  1package impl
  2
  3import (
  4	"chain/runtime"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/moul/helplink"
  9	"gno.land/p/moul/md"
 10	"gno.land/p/nt/bptree/v0/pager"
 11	"gno.land/p/nt/mux/v0"
 12	"gno.land/p/nt/seqid/v0"
 13	"gno.land/p/nt/ufmt/v0"
 14	"gno.land/r/gov/dao"
 15	"gno.land/r/sys/users"
 16)
 17
 18type render struct {
 19	relativeRealmPath string
 20	router            *mux.Router
 21	pssPager          *pager.Pager
 22}
 23
 24func NewRender(d *GovDAO) *render {
 25	ren := &render{
 26		pssPager: pager.NewPager(d.pss.BPTree, 5, true),
 27	}
 28
 29	r := mux.NewRouter()
 30
 31	// Handlers use mux's rlm-aware shape: rlm is supplied at RenderRlm
 32	// dispatch time rather than captured at NewRender time. This lets
 33	// downstream crossing reads (dao.GetProposal etc.) use cross(rlm)
 34	// without relying on bare cross or restructuring the router.
 35	r.HandleFuncRlm("", func(_ int, rlm realm, rw *mux.ResponseWriter, req *mux.Request) {
 36		rw.Write(ren.renderActiveProposals(0, rlm, req.RawPath, d))
 37	})
 38
 39	r.HandleFuncRlm("{pid}", func(_ int, rlm realm, rw *mux.ResponseWriter, req *mux.Request) {
 40		rw.Write(ren.renderProposalPage(0, rlm, req.GetVar("pid"), d))
 41	})
 42
 43	r.HandleFuncRlm("{pid}/votes", func(_ int, rlm realm, rw *mux.ResponseWriter, req *mux.Request) {
 44		rw.Write(ren.renderVotesForProposal(0, rlm, req.GetVar("pid"), d))
 45	})
 46
 47	ren.router = r
 48
 49	return ren
 50}
 51
 52func (ren *render) Render(_ int, rlm realm, pkgPath string, path string) string {
 53	relativePath, found := strings.CutPrefix(pkgPath, runtime.ChainDomain())
 54	if !found {
 55		panic(ufmt.Sprintf(
 56			"realm package with unexpected name found: %v in chain domain %v",
 57			pkgPath, runtime.ChainDomain()))
 58	}
 59	ren.relativeRealmPath = relativePath
 60	return ren.router.RenderRlm(0, rlm, path)
 61}
 62
 63func (ren *render) renderActiveProposals(_ int, rlm realm, url string, d *GovDAO) string {
 64	out := "# GovDAO\n"
 65	out += "## Members\n"
 66	out += "[> Go to Memberstore <](/r/gov/dao/v3/memberstore)\n"
 67	out += "## Proposals\n"
 68	page := ren.pssPager.MustGetPageByPath(url)
 69	if len(page.Items) == 0 {
 70		out += "\nNo proposals yet.\n\n"
 71		return out
 72	}
 73
 74	for _, item := range page.Items {
 75		seqpid, err := seqid.FromString(item.Key)
 76		if err != nil {
 77			continue
 78		}
 79		out += ren.renderProposalListItem(0, rlm, ufmt.Sprintf("%v", int64(seqpid)), d)
 80		out += "---\n\n"
 81	}
 82
 83	out += page.Picker("")
 84
 85	return out
 86}
 87
 88func (ren *render) renderProposalPage(_ int, rlm realm, sPid string, d *GovDAO) string {
 89	pid, err := strconv.ParseInt(sPid, 10, 64)
 90	if err != nil {
 91		return ufmt.Sprintf("# Error: Invalid proposal ID format.\n\n\n%s\n\n", err.Error())
 92	}
 93
 94	p, err := dao.GetProposal(dao.ProposalID(pid))
 95	if err != nil {
 96		return ufmt.Sprintf("# Proposal not found\n\n%s", err.Error())
 97	}
 98
 99	ps := d.pss.GetStatus(dao.ProposalID(pid))
100	out := ufmt.Sprintf("## Prop #%v - %v\n", pid, md.EscapeText(p.Title()))
101	out += "Author: " + tryResolveAddr(p.Author()) + "\n\n"
102
103	out += p.Description()
104	out += "\n\n"
105
106	// Add executor metadata if available
107	if p.ExecutorString() != "" {
108		out += ufmt.Sprintf(`This proposal contains the following metadata:
109
110%s
111
112Executor created in: %s
113`, p.ExecutorString(), p.ExecutorCreationRealm())
114		out += "\n\n"
115	}
116
117	out += "\n\n---\n\n"
118	out += ps.String(0, rlm)
119	out += "\n"
120	out += ufmt.Sprintf("[Detailed voting list](%v:%v/votes)", ren.relativeRealmPath, pid)
121	out += "\n\n---\n\n"
122
123	out += renderActionBar(ufmt.Sprintf("%v", pid))
124
125	return out
126}
127
128func (ren *render) renderProposalListItem(_ int, rlm realm, sPid string, d *GovDAO) string {
129	pid, err := strconv.ParseInt(sPid, 10, 64)
130	if err != nil {
131		return ufmt.Sprintf("# Error: Invalid proposal ID format.\n\n\n%s\n\n", err.Error())
132	}
133
134	p, err := dao.GetProposal(dao.ProposalID(pid))
135	if err != nil {
136		return ufmt.Sprintf("# Proposal not found\n\n%s\n\n", err.Error())
137	}
138
139	ps := d.pss.GetStatus(dao.ProposalID(pid))
140	out := ufmt.Sprintf("### [Prop #%v - %v](%v:%v)\n", pid, md.EscapeText(p.Title()), ren.relativeRealmPath, pid)
141	out += ufmt.Sprintf("Author: %s\n\n", tryResolveAddr(p.Author()))
142
143	out += "Status: " + getPropStatus(ps)
144	out += "\n\n"
145
146	out += "Tiers eligible to vote: "
147	out += strings.Join(ps.TiersAllowedToVote, ", ")
148
149	out += "\n\n"
150	return out
151}
152
153func (ren *render) renderVotesForProposal(_ int, rlm realm, sPid string, d *GovDAO) string {
154	pid, err := strconv.ParseInt(sPid, 10, 64)
155	if err != nil {
156		return ufmt.Sprintf("# Error: Invalid proposal ID format.\n\n\n%s\n\n", err.Error())
157	}
158
159	ps := d.pss.GetStatus(dao.ProposalID(pid))
160	if ps == nil {
161		return ufmt.Sprintf("# Proposal not found\n\nProposal %v does not exist.", pid)
162	}
163
164	out := ""
165	out += ufmt.Sprintf("# Proposal #%v - Vote List\n\n", pid)
166	out += StringifyVotes(0, rlm, ps)
167
168	return out
169}
170
171func isPropActive(ps *proposalStatus) bool {
172	return !ps.Accepted && !ps.Denied
173}
174
175func getPropStatus(ps *proposalStatus) string {
176	if ps == nil {
177		return "UNKNOWN"
178	}
179	if ps.Accepted {
180		return "ACCEPTED"
181	} else if ps.Denied {
182		return "REJECTED"
183	}
184	return "ACTIVE"
185}
186
187func renderActionBar(sPid string) string {
188	out := "### Actions\n"
189
190	proxy := helplink.Realm("gno.land/r/gov/dao")
191	out += proxy.Func("Vote YES", "MustVoteOnProposalSimple", "pid", sPid, "option", "YES") + " | "
192	out += proxy.Func("Vote NO", "MustVoteOnProposalSimple", "pid", sPid, "option", "NO") + " | "
193	out += proxy.Func("Vote ABSTAIN", "MustVoteOnProposalSimple", "pid", sPid, "option", "ABSTAIN")
194
195	out += "\n\n"
196	out += "WARNING: Please double check transaction data before voting."
197	return out
198}
199
200func tryResolveAddr(addr address) string {
201	userData := users.ResolveAddress(addr)
202	if userData == nil {
203		return addr.String()
204	}
205	return userData.RenderLink("")
206}