render.gno
13.96 Kb · 373 lines
1package hlmailbox
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/p/nt/avl/v0"
8 "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/decisions/v0"
9 hl "gno.land/p/g1zdaz6sypk5tdsjc2fzsqrrn4ttn8qzmnl4waqp/hyperlane/v0"
10)
11
12const (
13 pageMax = 20 // messages a "msgs/" page holds
14 verifiersMax = 20 // registered verifier paths (and announced successors) the page and "json" name, and a "verifiers/" ("successors/") page holds
15)
16
17// Render:
18//
19// "" the mailbox's page: the launch values live or waiting, verifiers, signers, seats, flags
20// "decisions" the decisions page: the last 100 pending and executed per kind
21// "json" domain, hook, count, root, live, signers, verifiers (the first 20 registered paths and the
22// count), seats, flags, routes (each with its predecessor and successor, and the height and
23// time of the kind 6 that enrolled it on Solana: enrolled, enrolledAt), successors (the
24// first 20 announced paths, the count, and successorRoutes: the route each announced for),
25// for the agents
26// "decisions.json" every decision, as a JSON array
27// "msg/<nonce>" one dispatched message: {"nonce","id","root","height","message"}; "null" if none
28// "msgs/<from>" up to 20 messages from nonce <from> on, as a JSON array
29// "verifiers/<from>" up to 20 registered verifier paths from offset <from> on, in path order, as a JSON array
30// "successors/<from>" up to 20 announced successor paths from offset <from> on, in path order, as a JSON array
31// "delivered/<id>" the block height a message id (hex) was delivered at here, 0 if never
32func Render(path string) string {
33 switch {
34 case path == "":
35 return page()
36 case path == "decisions":
37 return "# Decisions\n\n" + ledger.Page()
38 case path == "json":
39 return summaryJSON()
40 case path == "decisions.json":
41 return ledger.JSON()
42 case strings.HasPrefix(path, "msg/"):
43 n, ok := parseNonce(path[len("msg/"):])
44 if !ok {
45 return "null"
46 }
47 return msgJSON(n)
48 case strings.HasPrefix(path, "msgs/"):
49 from, ok := parseNonce(path[len("msgs/"):])
50 if !ok {
51 return "[]"
52 }
53 var b strings.Builder
54 b.WriteString("[")
55 for i := uint32(0); i < pageMax && uint64(from)+uint64(i) < uint64(tree.Count); i++ {
56 if i > 0 {
57 b.WriteString(",")
58 }
59 b.WriteString(msgJSON(from + i))
60 }
61 b.WriteString("]")
62 return b.String()
63 case strings.HasPrefix(path, "verifiers/"):
64 from, ok := parseNonce(path[len("verifiers/"):])
65 if !ok {
66 return "[]"
67 }
68 return pathsJSON(&verifiers, int(from))
69 case strings.HasPrefix(path, "successors/"):
70 from, ok := parseNonce(path[len("successors/"):])
71 if !ok {
72 return "[]"
73 }
74 return pathsJSON(&successors, int(from))
75 case strings.HasPrefix(path, "delivered/"):
76 return strconv.FormatInt(Delivered(path[len("delivered/"):]), 10)
77 }
78 return "unknown path; see the mailbox's page"
79}
80
81func parseNonce(s string) (uint32, bool) {
82 n, err := strconv.ParseUint(s, 10, 32)
83 return uint32(n), err == nil
84}
85
86func q(s string) string { return strconv.Quote(s) }
87
88func msgJSON(n uint32) string {
89 v := sent.Get(nonceKey(n))
90 if v == nil {
91 return "null"
92 }
93 s := v.(*sentMsg)
94 id := hl.ID([]byte(s.raw))
95 return "{\"nonce\":" + u32(n) + ",\"id\":" + q(hx(id[:])) + ",\"root\":" + q(hx(s.root[:])) +
96 ",\"height\":" + strconv.FormatInt(s.height, 10) + ",\"message\":" + q(hx([]byte(s.raw))) + "}"
97}
98
99func keysJSON(ks []Key) string {
100 parts := make([]string, len(ks))
101 for i, k := range ks {
102 parts[i] = q(hx(k[:]))
103 }
104 return "[" + strings.Join(parts, ",") + "]"
105}
106
107// pathsJSON is up to verifiersMax keys of a tree of pkgpaths (the registered verifiers, the announced successors)
108// from offset from on, in path order: the listing is bounded whatever the count, since nothing is ever removed.
109func pathsJSON(t *avl.Tree, from int) string {
110 var b strings.Builder
111 b.WriteString("[")
112 first := true
113 t.IterateByOffset(from, verifiersMax, func(k string, _ any) bool {
114 if !first {
115 b.WriteString(",")
116 }
117 first = false
118 b.WriteString(q(k))
119 return false
120 })
121 b.WriteString("]")
122 return b.String()
123}
124
125// pathsLine is a tree of pkgpaths on the page: the first verifiersMax in path order and, past them, where the rest
126// are (page is the Render path that pages them); "none" for an empty tree.
127func pathsLine(t *avl.Tree, page string) string {
128 n := t.Size()
129 if n == 0 {
130 return "none"
131 }
132 var b strings.Builder
133 i := 0
134 t.IterateByOffset(0, verifiersMax, func(k string, _ any) bool {
135 if i > 0 {
136 b.WriteString(", ")
137 }
138 i++
139 b.WriteString("`" + k + "`")
140 return false
141 })
142 if n > verifiersMax {
143 m := strconv.Itoa(verifiersMax)
144 b.WriteString(", … (the first " + m + "; the rest at [" + page + "/" + m + "](" + pkgPathLink() + ":" + page + "/" + m + "))")
145 }
146 return b.String()
147}
148
149func summaryJSON() string {
150 var b strings.Builder
151 b.WriteString("{\"domain\":" + u32(localDomain) + ",\"hook\":" + q(Hook()) + ",\"address\":" + q(string(Address())) +
152 ",\"count\":" + u32(tree.Count) + ",\"root\":" + q(Root()) + ",\"delivered\":" + strconv.Itoa(done.Size()) +
153 ",\"live\":" + strconv.FormatBool(live) + ",\"launchedAt\":" + strconv.FormatInt(launchedAt, 10) +
154 "," + decisions.GovernorJSON(launchGovernor, yielded) +
155 ",\"launchOrigin\":" + u32(launchOrigin) + ",\"clocks\":" + decisions.ClocksJSON() + ",\"decisions\":" + strconv.FormatInt(ledger.Last(), 10) +
156 ",\"signers\":{\"keys\":" + keysJSON(signerKeys) + ",\"threshold\":" + strconv.Itoa(signerRule.Threshold) +
157 ",\"groupA\":" + keysJSON(signerRule.GroupA) + ",\"groupB\":" + keysJSON(signerRule.GroupB) + "}" +
158 ",\"verifiers\":{")
159 first := true
160 active.Iterate("", "", func(k string, v any) bool {
161 origin, _ := strconv.ParseUint(k, 10, 32)
162 if !first {
163 b.WriteString(",")
164 }
165 first = false
166 b.WriteString(q(strconv.FormatUint(origin, 10)) + ":{\"pkgpath\":" + q(v.(string)) + ",\"registered\":" + strconv.FormatBool(verifiers.Has(v.(string))) + "}")
167 return false
168 })
169 b.WriteString("},\"registered\":" + pathsJSON(&verifiers, 0) + ",\"registeredCount\":" + strconv.Itoa(verifiers.Size()) + ",\"seats\":[")
170 for i := 1; i <= seatCount; i++ {
171 if i > 1 {
172 b.WriteString(",")
173 }
174 b.WriteString(q(string(seats[i])))
175 }
176 b.WriteString("],\"routes\":{")
177 first = true
178 routes.Iterate("", "", func(k string, v any) bool {
179 r := v.(*routeRec)
180 if !first {
181 b.WriteString(",")
182 }
183 first = false
184 b.WriteString(q(k) + ":{\"epoch\":" + strconv.FormatInt(r.epoch, 10) + ",\"epochAt\":" + strconv.FormatInt(r.epochAt, 10) +
185 ",\"sends\":" + strconv.FormatBool(Paused(k, FnSends)) + ",\"credits\":" + strconv.FormatBool(Paused(k, FnCredits)) +
186 ",\"payouts\":" + strconv.FormatBool(Paused(k, FnPayouts)) + ",\"flags\":[")
187 for i, f := range Flags(k) {
188 if i > 0 {
189 b.WriteString(",")
190 }
191 b.WriteString(q(f))
192 }
193 b.WriteString("],\"predecessor\":" + q(r.predecessor) + ",\"successor\":" + q(r.successor) +
194 ",\"enrolled\":" + strconv.FormatInt(r.enrolled, 10) + ",\"enrolledAt\":" + strconv.FormatInt(r.enrolledAt, 10) +
195 ",\"pulled\":" + strconv.FormatBool(r.pulled) + ",\"dropped\":" + quotedJSON(r.dropped) + "}")
196 return false
197 })
198 b.WriteString("},\"droppedRoutes\":" + droppedRoutesJSON() + ",\"successors\":" + pathsJSON(&successors, 0) + ",\"successorCount\":" + strconv.Itoa(successors.Size()) + ",\"successorRoutes\":" + successorRoutesJSON() + "}")
199 return b.String()
200}
201
202// quotedJSON is a list of strings as a JSON array.
203func quotedJSON(ss []string) string {
204 parts := make([]string, len(ss))
205 for i, s := range ss {
206 parts[i] = q(s)
207 }
208 return "[" + strings.Join(parts, ",") + "]"
209}
210
211// droppedRoutesJSON is every dropped successor (R35) with the predecessor whose re-point dropped it, as an object
212// of pkgpath → pkgpath: the tombstones, bounded by the migrations a bridge sees in its life.
213func droppedRoutesJSON() string {
214 var b strings.Builder
215 b.WriteString("{")
216 first := true
217 droppedRoutes.Iterate("", "", func(k string, v any) bool {
218 if !first {
219 b.WriteString(",")
220 }
221 first = false
222 b.WriteString(q(k) + ":" + q(v.(string)))
223 return false
224 })
225 b.WriteString("}")
226 return b.String()
227}
228
229// droppedLine is the dropped successors on the page, each with the predecessor that re-pointed away from it; ""
230// when none.
231func droppedLine() string {
232 if droppedRoutes.Size() == 0 {
233 return ""
234 }
235 var parts []string
236 droppedRoutes.Iterate("", "", func(k string, v any) bool {
237 parts = append(parts, "`"+k+"` (by `"+v.(string)+"`)")
238 return false
239 })
240 return "- dropped (routes no more, re-pointed away): " + strings.Join(parts, ", ") + "\n"
241}
242
243// successorRoutesJSON is the first verifiersMax announced successors with the route each announced for (AQ5), as
244// an object of pkgpath → "GNOT" or "BUBBLE".
245func successorRoutesJSON() string {
246 var b strings.Builder
247 b.WriteString("{")
248 first := true
249 successors.IterateByOffset(0, verifiersMax, func(k string, v any) bool {
250 if !first {
251 b.WriteString(",")
252 }
253 first = false
254 b.WriteString(q(k) + ":" + q(v.(string)))
255 return false
256 })
257 b.WriteString("}")
258 return b.String()
259}
260
261// successorsLine is the announced successors on the page, each with the route it announced for (AQ5): the first
262// verifiersMax in path order and, past them, where the rest are; "none" for an empty tree.
263func successorsLine() string {
264 n := successors.Size()
265 if n == 0 {
266 return "none"
267 }
268 var b strings.Builder
269 i := 0
270 successors.IterateByOffset(0, verifiersMax, func(k string, v any) bool {
271 if i > 0 {
272 b.WriteString(", ")
273 }
274 i++
275 b.WriteString("`" + k + "` (" + v.(string) + ")")
276 return false
277 })
278 if n > verifiersMax {
279 m := strconv.Itoa(verifiersMax)
280 b.WriteString(", … (the first " + m + "; the rest at [successors/" + m + "](" + pkgPathLink() + ":successors/" + m + "))")
281 }
282 return b.String()
283}
284
285func page() string {
286 var b strings.Builder
287 b.WriteString("# The bridge's mailbox\n\n")
288 b.WriteString("gno.land's end of the GNOT and BUBBLE bridge to Solana: messages out to Solana (the Dispatch event the light client proves), and in from it under the active verifier. Every setting here moves only by a decision made with this realm's builders — the governor's own until it yields, a GovDAO proposal after; the guardians and watch keys can only pause.\n\n")
289 if live {
290 b.WriteString("- **live** since " + strconv.FormatInt(launchedAt, 10) + " (the Launch decision executed)\n")
291 } else {
292 b.WriteString("- **not launched**: the values below are the compiled launch values, waiting for the Launch decision; the routes refuse sends and credits. A passed Launch executes within 14 days of its proposal, like every decision; one nobody executed by then lapses and is proposed again\n")
293 }
294 b.WriteString("- " + decisions.GovernorLine(launchGovernor, yielded) + "\n")
295 b.WriteString("- a proposal made with a builder (of this realm, a route, r/hlfees or r/hlgov) is executed with r/gov/dao's `ExecuteOrRejectProposal`: the one error a closure returns — the 14-day deadline — then denies it for good; under `ExecuteProposal` that error reverts the transaction instead and the proposal stands, every retry failing the same way. Anything else a closure refuses is a panic, which leaves the proposal for a retry under either call\n")
296 b.WriteString("- domain: " + u32(localDomain) + "; Solana: " + u32(launchOrigin) + "\n")
297 b.WriteString("- " + decisions.Clocks() + "\n")
298 b.WriteString("- merkle tree hook: `" + Hook() + "`\n")
299 b.WriteString("- messages out: " + u32(tree.Count) + ", root `" + Root() + "`; in: " + strconv.Itoa(done.Size()) + "\n")
300 b.WriteString("- decisions: " + strconv.FormatInt(ledger.Last(), 10) + " ([page](" + pkgPathLink() + ":decisions))\n")
301 b.WriteString("\n## Verifiers\n\n")
302 active.Iterate("", "", func(k string, v any) bool {
303 origin, _ := strconv.ParseUint(k, 10, 32)
304 note := ""
305 if !verifiers.Has(v.(string)) {
306 note = " (**not registered**: nothing from this origin is taken until it is)"
307 }
308 b.WriteString("- from " + strconv.FormatUint(origin, 10) + ": `" + v.(string) + "`" + note + "\n")
309 return false
310 })
311 b.WriteString("- registered (" + strconv.Itoa(verifiers.Size()) + "): " + pathsLine(&verifiers, "verifiers"))
312 b.WriteString("\n\n## Signers\n\n- rule: " + signerRule.String() + "\n")
313 for i, k := range signerKeys {
314 b.WriteString("- " + strconv.Itoa(i) + ": `" + hx(k[:]) + "`\n")
315 }
316 b.WriteString("\n## Seats\n\n")
317 for i := 1; i <= seatCount; i++ {
318 kind := "guardian"
319 if !IsGuardianSeat(i) {
320 kind = "watch key"
321 }
322 a := string(seats[i])
323 if a == "" {
324 a = "(empty)"
325 }
326 b.WriteString("- seat " + strconv.Itoa(i) + " (" + kind + "): " + a + "\n")
327 }
328 b.WriteString("\n## Routes and flags\n\n")
329 routes.Iterate("", "", func(k string, v any) bool {
330 r := v.(*routeRec)
331 b.WriteString("- `" + k + "`: payouts epoch " + strconv.FormatInt(r.epoch, 10))
332 if r.predecessor != "" {
333 b.WriteString(", successor of `" + r.predecessor + "`")
334 }
335 if r.pulled {
336 b.WriteString(", page 0 pulled")
337 }
338 if r.successor != "" {
339 b.WriteString(", migrated to `" + r.successor + "`")
340 }
341 if len(r.dropped) > 0 {
342 b.WriteString(", dropped `" + strings.Join(r.dropped, "`, `") + "`")
343 }
344 if r.enrolled != 0 {
345 b.WriteString(", enrolled on Solana by kind 6 at height " + strconv.FormatInt(r.enrolled, 10) + " (" + strconv.FormatInt(r.enrolledAt, 10) + ")")
346 }
347 fs := Flags(k)
348 if len(fs) == 0 {
349 b.WriteString(", no flag\n")
350 } else {
351 b.WriteString("\n")
352 for _, f := range fs {
353 b.WriteString(" - **paused** " + f + "\n")
354 }
355 }
356 return false
357 })
358 b.WriteString(droppedLine())
359 b.WriteString("- successors announced (" + strconv.Itoa(successors.Size()) + "): " + successorsLine() + " (a route's migrate names only one of them, announced for its own route)\n")
360 if tree.Count > 0 {
361 b.WriteString("\n## Latest out\n\n")
362 sent.ReverseIterateByOffset(0, 10, func(k string, v any) bool {
363 s := v.(*sentMsg)
364 m, _ := hl.ParseMessage([]byte(s.raw))
365 id := hl.ID([]byte(s.raw))
366 b.WriteString("- #" + u32(m.Nonce) + " to " + u32(m.Destination) + " at block " + strconv.FormatInt(s.height, 10) + ": `" + hx(id[:]) + "`\n")
367 return false
368 })
369 }
370 return b.String()
371}
372
373func pkgPathLink() string { return strings.TrimPrefix(pkgPath, "gno.land") }