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}