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