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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

devtools.gno

9.96 Kb · 264 lines
  1// Package devtools is the worked example of wiring moul's developer packages
  2// into one realm: what a new realm should copy.
  3//
  4// Everything here is a COMBINATION. Any one of these packages has its own demo
  5// showing it alone; the thing that is hard to find is what they look like used
  6// together, which is the only form a real realm ever meets them in. So this
  7// realm imports ten of them and every one earns its place in the output:
  8//
  9//	r/moul/config     the notice block, the pause switch, the explorer to link
 10//	p/moul/mygnoscan  the explorer routes themselves
 11//	p/moul/debug      a metadata panel behind ?debug=1
 12//	p/moul/realmpath  reading the query string Render was handed
 13//	p/moul/kit/ui     escaping what a caller typed, and the tables
 14//	p/moul/md         the markdown, without hand-rolled strings
 15//	p/moul/txlink     the "call this function" link
 16//	p/moul/fifo       the bounded note list, so the state cannot grow forever
 17//	p/moul/kit/tally  the note leaderboard, ties ordered rather than arbitrary
 18//	p/moul/kit/num    pricing that list in GNOT rather than raw ugnot
 19//
 20// # The three lines worth copying
 21//
 22// A realm that does nothing else should still do these:
 23//
 24//	func Render(path string) string {
 25//		return config.TopBlockFor(realmPath) + body + config.BottomBlockFor(realmPath)
 26//	}
 27//
 28//	func Mutate(cur realm, s string) {
 29//		config.AssertWritableFor(realmPath)  // honours the pause switch
 30//		notes.Append(ui.Inline(s))           // escapes what a caller typed
 31//	}
 32//
 33// The first gives every realm a place to put a warning and a pause banner
 34// without shipping banner code. The second is two separate guards that both
 35// have to be there: the pause one so an incident can stop writes from outside
 36// the realm, and the escaping one because a string a caller typed is the
 37// oldest way to break a page.
 38package devtools
 39
 40import (
 41	"strconv"
 42	"strings"
 43
 44	"chain/markdown"
 45	"chain/runtime"
 46	"chain/runtime/unsafe"
 47
 48	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/debug/v0"
 49	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/fifo/v0"
 50	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/num/v0"
 51	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/tally/v0"
 52	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/ui/v0"
 53	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/md/v0"
 54	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/mygnoscan/v0"
 55	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/realmpath/v0"
 56	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/txlink/v0"
 57	"gno.land/r/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/config/v1"
 58)
 59
 60// realmPath is this realm's own path.
 61//
 62// Passed explicitly to config rather than relying on its zero-argument helpers.
 63// Both work (a plain read is borrowed, so config sees this realm as the
 64// caller), but a constant cannot be wrong, and spelling it out is what a reader
 65// copying this file needs to see.
 66const realmPath = "gno.land/r/moul/x/allinone/devtools/v0"
 67
 68// maxNotes bounds the guest list. fifo drops the oldest rather than growing,
 69// because storage on chain is paid for and never refunded: an unbounded list in
 70// a demo anyone can write to is a bill with no ceiling.
 71const maxNotes = 10
 72
 73var notes = fifo.New(maxNotes)
 74
 75// authors counts notes per writer. A tally.Board rather than a map: gno map
 76// iteration order is unspecified, so a leaderboard built by ranging one is not
 77// reproducible between calls.
 78var authors = tally.NewBoard()
 79
 80// depositPerByte is what the chain charges to keep a byte, in ugnot. Used only
 81// to price the note list on the page, never to settle anything.
 82const depositPerByte = 100
 83
 84// Add appends a note. It is the only mutating function here, and it carries
 85// both guards a real one should.
 86func Add(cur realm, body string) {
 87	// 1. The pause switch. Set `pause@r/moul/x/allinone/devtools` on
 88	//    r/moul/config and this aborts, with the reason, from outside this
 89	//    realm and without a redeploy.
 90	config.AssertWritableFor(realmPath)
 91
 92	body = strings.TrimSpace(body)
 93	if strings.TrimSpace(markdown.StripBidiAndZeroWidth(body)) == "" {
 94		panic("empty note")
 95	}
 96	if len(body) > 200 {
 97		panic("note too long: max 200 bytes, got " + strconv.Itoa(len(body)))
 98	}
 99
100	// 2. Escaping. body is a string a caller typed, and it is about to land in
101	//    a markdown list. ui.Inline is the repo's standard for exactly this.
102	notes.Append(ui.Inline(body))
103
104	// 3. The scoreboard. tally.Board keeps one score per author and orders
105	//    ties by key, so two authors on the same count always render in the
106	//    same order.
107	authors.Add(unsafe.PreviousRealm().Address().String(), 1)
108}
109
110// Reset empties the list, so the realm can be put back to a known state
111// without a redeploy. Not gated by the pause switch: clearing up is the thing
112// you still want to do while paused.
113func Reset(cur realm) {
114	notes = fifo.New(maxNotes)
115	authors = tally.NewBoard()
116}
117
118// Render shows every package at work. The root view is deliberately free of
119// anything that moves on its own, so an example test can pin it; the live
120// numbers live under "chain".
121func Render(path string) string {
122	req := realmpath.Parse(path)
123
124	var b strings.Builder
125	b.WriteString(config.TopBlockFor(realmPath))
126	b.WriteString(md.H1("allinone: devtools"))
127	b.WriteString("\nTen packages, used together rather than one at a time. ")
128	b.WriteString(md.Link("source", config.MygnoscanFor(realmPath)))
129	b.WriteString("\n\n")
130
131	switch req.PathPart(0) {
132	case "chain":
133		b.WriteString(renderChain())
134	default:
135		b.WriteString(renderHome(path))
136	}
137
138	// debug prints its own panel only when ?debug=1 is set, and the empty
139	// string otherwise, so this line is safe to leave in a deployed realm.
140	b.WriteString(debug.Render(path))
141	b.WriteString(config.BottomBlockFor(realmPath))
142	return b.String()
143}
144
145func renderHome(path string) string {
146	var b strings.Builder
147
148	b.WriteString(md.H2("Notes"))
149	b.WriteString("\n")
150	// renderNotes returns no trailing newline of its own: md.BulletList and
151	// ui.Empty already end in one, and adding a second here produced two
152	// consecutive blank lines, which gno collapses inside an // Output: block
153	// and which therefore no example test can ever pin.
154	b.WriteString(renderNotes())
155	b.WriteString("\n")
156
157	// txlink builds the "call this function" URL for the CURRENT realm, so it
158	// keeps working after a version bump without anything here changing.
159	b.WriteString(md.BulletList([]string{
160		md.Link("add a note", txlink.NewLink("Add").AddArgs("body", "hello").URL()),
161		md.Link("clear them", txlink.Call("Reset")),
162		md.Link("live chain values", "/r/moul/x/allinone/devtools/v0:chain"),
163		md.Link("toggle the debug panel", debug.ToggleURL(path)),
164	}))
165	b.WriteString("\n")
166
167	b.WriteString(renderBoard())
168
169	b.WriteString(md.H2("What each package does here"))
170	b.WriteString("\n")
171	t := ui.NewTable("package", "what it contributed")
172	t.Row("`r/moul/config`", "the notice block above, the pause guard on `Add`, and which explorer to link")
173	t.Row("`p/moul/mygnoscan`", "the explorer routes, so a link survives a route change")
174	t.Row("`p/moul/debug`", "the `?debug=1` panel, empty otherwise")
175	t.Row("`p/moul/realmpath`", "splitting the render path into a view and a query")
176	t.Row("`p/moul/kit/ui`", "escaping a caller's note, and this table")
177	t.Row("`p/moul/md`", "every heading, link and list on the page")
178	t.Row("`p/moul/txlink`", "the call links above, relative to this realm")
179	t.Row("`p/moul/fifo`", "the note list, bounded so storage cannot grow forever")
180	t.Row("`p/moul/kit/tally`", "the note leaderboard, with ties ordered by address")
181	t.Row("`p/moul/kit/num`", "pricing the note list in GNOT instead of raw ugnot")
182	b.WriteString(t.String())
183
184	return b.String()
185}
186
187// renderBoard shows who has written the most notes, and what the list costs to
188// keep. Both numbers come from a package rather than from arithmetic here.
189func renderBoard() string {
190	var b strings.Builder
191	b.WriteString(md.H2("Who has written the most"))
192	b.WriteString("\n")
193
194	top := authors.Top(5)
195	if len(top) == 0 {
196		b.WriteString(ui.Empty("No notes yet, so nobody is on the board."))
197	} else {
198		t := ui.NewTable("#", "author", "notes")
199		for i, e := range top {
200			t.Row(ui.Podium(i+1), ui.AddrOf(e.Key), num.Pad(e.Score, 2))
201		}
202		b.WriteString(t.String())
203	}
204
205	// num.GNOTf, not a hand-rolled divide by a million. The storage deposit is
206	// what the chain charges to keep these bytes, so it is the honest price of
207	// the list above.
208	b.WriteString("\nStorage deposit for the list: **")
209	b.WriteString(num.GNOTf(int64(notesBytes()) * depositPerByte))
210	b.WriteString("**\n\n")
211	return b.String()
212}
213
214// notesBytes is the length of every note currently held.
215func notesBytes() int {
216	n := 0
217	for _, v := range notes.Entries() {
218		n += len(v.(string))
219	}
220	return n
221}
222
223// renderChain is everything that moves: the height, and the links built from
224// it. Kept off the root view so the root can be pinned by an example.
225func renderChain() string {
226	s := config.Scanner()
227	h := runtime.ChainHeight()
228
229	var b strings.Builder
230	b.WriteString(md.H2("Live"))
231	b.WriteString("\n")
232
233	t := ui.NewTable("what", "value")
234	t.Row("chain id", ui.Cell(runtime.ChainID()))
235	t.Row("height", strconv.FormatInt(h, 10))
236	t.Row("explorer", ui.Cell(s.Base()))
237	t.Row("network", ui.Cell(s.Network()))
238	b.WriteString(t.String())
239	b.WriteString("\n")
240
241	// mygnoscan.Link used directly, rather than through config, to show that
242	// the builder is an ordinary value: config only decides where it points.
243	b.WriteString(md.BulletList([]string{
244		mygnoscan.Link("this block", s.Block(h)),
245		mygnoscan.Link("this realm's transactions", s.Realm(realmPath, mygnoscan.TabCalls)),
246		mygnoscan.Link("its dependency graph", s.Realm(realmPath, mygnoscan.TabDeps)),
247		mygnoscan.Link("the parked deploy queue", s.Packages(mygnoscan.PackagesInert)),
248	}))
249	b.WriteString("\n")
250	b.WriteString(md.Link("back", "/r/moul/x/allinone/devtools/v0"))
251	b.WriteString("\n")
252	return b.String()
253}
254
255func renderNotes() string {
256	if notes.Size() == 0 {
257		return ui.Empty("no notes yet")
258	}
259	items := []string{}
260	for _, e := range notes.Entries() {
261		items = append(items, e.(string))
262	}
263	return md.BulletList(items)
264}