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