Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions 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}