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