Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}