reads.gno
6.93 Kb · 229 lines
1package memba_gov
2
3import (
4 "strconv"
5 "time"
6)
7
8// Reads make no cross-realm call and never start or advance a clock. Ids and
9// unix times are JSON strings (clients parse them as big integers). Stored
10// text is printable ASCII, so the escapers work byte by byte.
11
12// MaxListed is the most proposals ProposalsJSON and Render return at once.
13const MaxListed = 20
14
15// RosterJSON lists seated members and open invitations.
16func RosterJSON() string {
17 W, N := seatedTotals()
18 b := []byte(`{"weight":` + strconv.Itoa(W) + `,"persons":` + strconv.Itoa(N) + `,"members":[`)
19 for i, p := range seated {
20 if i > 0 {
21 b = append(b, ',')
22 }
23 b = append(b, `{"id":`...)
24 b = q(b, p.id)
25 b = append(b, `,"address":`...)
26 b = q(b, string(p.addr))
27 b = append(b, `,"weight":`+strconv.Itoa(p.weight)+`,"lastActive":"`+unix(p.lastActive)+`"}`...)
28 }
29 b = append(b, `],"invitations":[`...)
30 n := 0
31 for _, inv := range invites {
32 if now() > inv.expires {
33 continue
34 }
35 if n > 0 {
36 b = append(b, ',')
37 }
38 n++
39 b = append(b, `{"id":`...)
40 b = q(b, inv.id)
41 b = append(b, `,"address":`...)
42 b = q(b, string(inv.addr))
43 b = append(b, `,"weight":`+strconv.Itoa(inv.weight)+`,"expires":"`+unix(inv.expires)+`"}`...)
44 }
45 return string(append(b, `]}`...))
46}
47
48// ProposalJSON returns one proposal with its ballots and current status, or
49// null for an unknown id.
50func ProposalJSON(pid uint64) string {
51 v, ok := proposals.Get(key(pid))
52 if !ok {
53 return "null"
54 }
55 return string(proposalJSON(nil, v.(*proposal)))
56}
57
58// ConstantsJSON returns the fixed policy: durations in seconds, roster bounds.
59func ConstantsJSON() string {
60 sec := func(d time.Duration) string { return unix(secs(d)) }
61 return `{"votingPeriod":` + sec(VotingPeriod) + `,"executionWindow":` + sec(ExecutionWindow) +
62 `,"criticalWeightDelay":` + sec(WeightedDelay) + `,"criticalHeadcountDelay":` + sec(HeadcountDelay) +
63 `,"rosterDelay":` + sec(RosterDelay) + `,"inactiveDelay":` + sec(InactiveDelay) +
64 `,"inactiveAfter":` + sec(InactiveAfter) + `,"minSeats":` + strconv.Itoa(MinSeats) +
65 `,"maxSeats":` + strconv.Itoa(MaxSeats) + `,"maxListed":` + strconv.Itoa(MaxListed) + `}`
66}
67
68// ProposalsJSON lists up to limit proposals (at most MaxListed) with ids
69// below before, newest first; before = 0 starts from the newest.
70func ProposalsJSON(before uint64, limit int) string {
71 if limit < 1 || limit > MaxListed {
72 limit = MaxListed
73 }
74 b := []byte(`{"total":"` + strconv.FormatUint(lastID, 10) + `","proposals":[`)
75 if before != 1 {
76 end := "" // a descending range includes its end
77 if before > 1 {
78 end = key(before - 1)
79 }
80 n := 0
81 proposals.ReverseIterate("", end, func(_ string, v any) bool {
82 if n > 0 {
83 b = append(b, ',')
84 }
85 b = proposalJSON(b, v.(*proposal))
86 n++
87 return n >= limit
88 })
89 }
90 return string(append(b, `]}`...))
91}
92
93func proposalJSON(b []byte, p *proposal) []byte {
94 st, readyAt := state(p)
95 b = append(b, `{"id":"`+strconv.FormatUint(p.id, 10)+`","target":`...)
96 b = q(b, p.target)
97 b = append(b, `,"action":`...)
98 b = q(b, p.action)
99 b = append(b, `,"args":`...)
100 b = q(b, p.args)
101 b = append(b, `,"scope":`...)
102 b = q(b, p.scope)
103 b = append(b, `,"class":`+strconv.Itoa(p.class)+`,"proposer":`...)
104 b = q(b, p.proposer)
105 b = append(b, `,"note":`...)
106 b = q(b, p.note)
107 b = append(b, `,"created":"`+unix(p.created)+`","deadline":"`+unix(p.created+secs(VotingPeriod))+
108 `","status":"`+st+`","readyAt":"`+unix(readyAt)+`","ballots":[`...)
109 for i, bl := range p.ballots {
110 if i > 0 {
111 b = append(b, ',')
112 }
113 b = append(b, `{"person":`...)
114 b = q(b, bl.person)
115 b = append(b, `,"vote":"`+bl.vote+`","since":"`+unix(bl.since)+`"}`...)
116 }
117 return append(b, `]}`...)
118}
119
120// Render shows the roster and the latest proposals. Each proposal's line
121// holds only fields the realm sets (id, status, class, proposer id); every
122// value a proposer wrote follows as its own labelled, escaped item, so none
123// can forge a field.
124func Render(_ string) string {
125 W, N := seatedTotals()
126 b := []byte("# Memba DAO governance\n\n## Members: " + strconv.Itoa(N) + " seated, weight " + strconv.Itoa(W) + "\n\n")
127 for _, p := range seated {
128 b = append(b, "- "...)
129 b = md(b, p.id)
130 b = append(b, " (weight "+strconv.Itoa(p.weight)+"): "...)
131 b = md(b, string(p.addr))
132 b = append(b, '\n')
133 }
134 b = append(b, "\n## Latest proposals\n\n"...)
135 n := 0
136 proposals.ReverseIterate("", "", func(_ string, v any) bool {
137 p := v.(*proposal)
138 st, _ := state(p)
139 b = append(b, "- #"+strconv.FormatUint(p.id, 10)+" "+st+", class "+strconv.Itoa(p.class)+", by "...)
140 b = md(b, p.proposer)
141 b = append(b, '\n')
142 for _, f := range [][2]string{{"target", p.target}, {"action", p.action}, {"args", p.args}, {"scope", p.scope}, {"proposer's note", p.note}} {
143 if f[1] != "" {
144 b = append(b, " - "+f[0]+": "...)
145 b = md(b, f[1])
146 b = append(b, '\n')
147 }
148 }
149 n++
150 return n >= MaxListed
151 })
152 if n == 0 {
153 b = append(b, "None yet.\n"...)
154 }
155 return string(b)
156}
157
158// unix formats a unix-second time (0 = none).
159func unix(t int64) string { return strconv.FormatInt(t, 10) }
160
161// grow makes room for n more bytes. The VM's append reallocates to the exact
162// length, so appending byte by byte past capacity would copy the whole
163// buffer each time; doubling keeps the escapers linear.
164func grow(b []byte, n int) []byte {
165 if cap(b)-len(b) >= n {
166 return b
167 }
168 nb := make([]byte, len(b), 2*cap(b)+n)
169 copy(nb, b)
170 return nb
171}
172
173// Escaper classes of each ASCII byte, one table lookup per byte: '.' is
174// copied, 'b' gets a backslash, 'u' becomes \u00XX, 'e' a numeric
175// character reference. Stored text is printable ASCII (0x7f masks the
176// index; no other byte reaches here).
177const (
178 qClass = "uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu" +
179 "..b...u.....................u.u." +
180 "............................b..." +
181 "...............................u"
182 // Markdown punctuation is backslash-escaped; < > & and g become
183 // references, so no HTML, entity or " g1..." address mention (a gnoweb
184 // user link) can form.
185 mdClass = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +
186 ".bbbb.ebbbbb.bbb..........b.ebe." +
187 "b..........................bbbbb" +
188 "b......e...................bbbbe"
189 hexDigits = "0123456789abcdef"
190)
191
192// q appends s as a JSON string; < > & are escaped so the text stays inert
193// in HTML.
194func q(b []byte, s string) []byte {
195 b = grow(b, 6*len(s)+2)
196 b = append(b, '"')
197 for i := 0; i < len(s); i++ {
198 switch c := s[i]; qClass[c&0x7f] {
199 case '.':
200 b = append(b, c)
201 case 'b':
202 b = append(b, '\\', c)
203 default:
204 b = append(b, '\\', 'u', '0', '0', hexDigits[c>>4], hexDigits[c&0xf])
205 }
206 }
207 return append(b, '"')
208}
209
210// md appends s with every character Markdown, HTML or gnoweb could act on
211// made literal.
212func md(b []byte, s string) []byte {
213 b = grow(b, 6*len(s))
214 for i := 0; i < len(s); i++ {
215 switch c := s[i]; mdClass[c&0x7f] {
216 case '.':
217 b = append(b, c)
218 case 'b':
219 b = append(b, '\\', c)
220 default: // &#NN; or &#NNN;
221 b = append(b, '&', '#')
222 if c >= 100 {
223 b = append(b, '0'+c/100)
224 }
225 b = append(b, '0'+c/10%10, '0'+c%10, ';')
226 }
227 }
228 return b
229}