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.

home.gno

12.29 Kb · 367 lines
  1// Package home is the realm behind https://gno.land/u/clockwork.
  2//
  3// gnoweb builds a user profile page by calling Render("") on the realm at the
  4// exact path /r/<username>/home (gno.land/pkg/gnoweb/handler_http.go,
  5// GetUserView). That lookup does no version resolution, so this path is the
  6// interface with gnoweb and can never carry a /vN suffix. Everything that
  7// might want to change therefore lives behind the path, in four layers:
  8//
  9//  1. CONTENT is data. The page is assembled from SLOTS: named markdown
 10//     fragments in an avl tree, each written by its own Set call. Updating
 11//     one paragraph is one small transaction.
 12//  2. LAYOUT is data. The slot named "layout" is the page template; every
 13//     :slug: placeholder in it is filled from the slot of that name.
 14//  3. STYLE is data. Slots under the "style." prefix are knobs (colors,
 15//     header mode) that the theme reads. Changing the accent color is a Set.
 16//  4. RENDERING is code, but replaceable. A THEME is a separate realm nested
 17//     under this one that implements Theme and registers itself from its own
 18//     init. The authority accepts it through p/clockwork/upgradeable and can
 19//     roll it back. The slots never move: an upgrade replaces the code that
 20//     reads them, not the data. When no theme is live the built-in renderer
 21//     in render.gno serves the page, and the operator views (system, slots,
 22//     edit, manifest) are always served by this realm, so a theme that
 23//     panics can be rolled back from the page it cannot break.
 24//
 25// This realm holds state and forwards. It is deliberately small, because it
 26// is the one piece that can never be redeployed: it is importable by its
 27// themes, so it cannot be private, so AddPackage refuses a second deploy.
 28package home
 29
 30import (
 31	"crypto/sha256"
 32	"encoding/hex"
 33	"strconv"
 34	"strings"
 35
 36	"chain/runtime"
 37
 38	"gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0"
 39	"gno.land/p/nt/avl/v0"
 40)
 41
 42// Admin is the initial authority: the only address that may write slots and
 43// accept, roll back or freeze a theme. It is a constant so a reviewer can read
 44// who controls the page without decoding chain state; TransferAuthority moves
 45// it afterwards.
 46//
 47// The value here is the standard test1 account, so the gnodev walkthrough in
 48// the README works unedited. `make build ADMIN=g1...` rewrites it for a real
 49// deployment.
 50const Admin = address("g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm")
 51
 52const (
 53	// LayoutSlug names the slot used as the page template.
 54	LayoutSlug = "layout"
 55
 56	// StylePrefix marks slots that are style knobs rather than content.
 57	// Style("accent", ...) reads the slot "style.accent".
 58	StylePrefix = "style."
 59
 60	// maxSlugLen bounds a slot name so an index row stays readable.
 61	maxSlugLen = 64
 62)
 63
 64// Theme is what a renderer realm promises. It is declared here, in the file
 65// that can never change, so every theme version agrees on one type identity.
 66//
 67// Render receives the render path ("" is the profile page) and returns
 68// markdown. The operator views (system, slots, edit, manifest) never reach a
 69// theme; for any other path it does not draw itself, a theme should return
 70// Fallback(path). A panic here aborts the query: there is no recovering a
 71// foreign realm's panic, so a broken theme shows an error until Rollback.
 72type Theme interface {
 73	Render(path string) string
 74}
 75
 76// Slot is one named fragment of the page.
 77type Slot struct {
 78	Body   string
 79	Rev    int   // Revision() at the time of the last write to this slot
 80	Height int64 // block height of that write
 81}
 82
 83var (
 84	proxy    *upgradeable.Proxy
 85	selfPath string
 86
 87	slots      = avl.NewTree() // slug -> *Slot
 88	rev        int             // total writes accepted; bumps on every mutation
 89	lastHeight int64           // height of the most recent write
 90)
 91
 92func init(cur realm) {
 93	// Captured rather than written as a constant, so links and the nesting
 94	// rule follow the path this code was actually deployed at.
 95	selfPath = cur.PkgPath()
 96	proxy = upgradeable.New(upgradeable.NewAddrAuthority(Admin))
 97}
 98
 99// ---------------------------------------------------------------------------
100// Authority
101
102// assertAuthority panics unless the caller of the crossing function that owns
103// cur is the current authority. It asks the proxy's Authority directly rather
104// than going through proxy.AssertAuthorized, so content writes keep working
105// after Freeze: freezing ends code upgrades, not editing.
106func assertAuthority(cur realm) {
107	caller := cur.Previous()
108	if !proxy.Authority().Authorized(caller.Address(), caller.PkgPath()) {
109		panic("home: restricted to the authority")
110	}
111}
112
113// ---------------------------------------------------------------------------
114// Slugs
115
116// reservedSlugs are placeholders computed at render time (see render.gno). A
117// slot may not take one of these names, so the page never depends on which
118// of the two would win.
119var reservedSlugs = []string{"chainid", "height", "owner", "realm", "rev", "slots", "theme", "updated"}
120
121// validSlug reports whether slug is a legal slot name: 1..maxSlugLen bytes of
122// [a-z0-9._-]. No ':' so a slug can never break out of its own :slug:
123// placeholder, and no uppercase so a slot has exactly one name.
124func validSlug(slug string) bool {
125	if len(slug) == 0 || len(slug) > maxSlugLen {
126		return false
127	}
128	for i := 0; i < len(slug); i++ {
129		c := slug[i]
130		switch {
131		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
132		case c == '-', c == '_', c == '.':
133		default:
134			return false
135		}
136	}
137	return true
138}
139
140func assertWritableSlug(slug string) {
141	if !validSlug(slug) {
142		panic("home: invalid slug " + strconv.Quote(slug) +
143			": want 1-" + strconv.Itoa(maxSlugLen) + " bytes of [a-z0-9._-]")
144	}
145	for _, r := range reservedSlugs {
146		if slug == r {
147			panic("home: reserved slug " + slug + " is computed at render time")
148		}
149	}
150}
151
152// ---------------------------------------------------------------------------
153// Writes (authority only)
154
155// Set creates or replaces the slot named slug. This is the ordinary update:
156// one slot, one transaction. An empty body keeps the slot but empties it; use
157// Delete to remove it.
158func Set(cur realm, slug, body string) {
159	assertAuthority(cur)
160	assertWritableSlug(slug)
161	write(slug, body)
162}
163
164// Append adds to the end of a slot, creating it when absent. It exists for a
165// body too large for one transaction: Set the first chunk, Append the rest.
166func Append(cur realm, slug, body string) {
167	assertAuthority(cur)
168	assertWritableSlug(slug)
169	write(slug, Get(slug)+body)
170}
171
172// Delete removes a slot. Deleting the layout slot restores the theme's
173// default layout.
174func Delete(cur realm, slug string) {
175	assertAuthority(cur)
176	if _, removed := slots.Remove(slug); !removed {
177		panic("home: no such slot: " + slug)
178	}
179	bump()
180}
181
182func write(slug, body string) {
183	bump()
184	slots.Set(slug, &Slot{Body: body, Rev: rev, Height: lastHeight})
185}
186
187func bump() {
188	rev++
189	lastHeight = runtime.ChainHeight()
190}
191
192// ---------------------------------------------------------------------------
193// Reads (anyone, including themes)
194
195// Get returns a slot body, or "" when the slot does not exist.
196func Get(slug string) string {
197	v := slots.Get(slug)
198	if v == nil {
199		return ""
200	}
201	return v.(*Slot).Body
202}
203
204// Has reports whether a slot exists, empty or not.
205func Has(slug string) bool { return slots.Has(slug) }
206
207// Lookup returns a copy of a slot and whether it exists.
208func Lookup(slug string) (Slot, bool) {
209	v := slots.Get(slug)
210	if v == nil {
211		return Slot{}, false
212	}
213	return *v.(*Slot), true
214}
215
216// Slugs returns every slot name, sorted.
217func Slugs() []string {
218	out := make([]string, 0, slots.Size())
219	slots.Iterate("", "", func(key string, _ any) bool {
220		out = append(out, key)
221		return false
222	})
223	return out
224}
225
226// Each visits every slot in name order until fn returns true.
227func Each(fn func(slug string, s Slot) bool) {
228	slots.Iterate("", "", func(key string, value any) bool {
229		return fn(key, *value.(*Slot))
230	})
231}
232
233// Style returns the style knob named key (the slot "style."+key), or fallback
234// when it is absent or empty.
235func Style(key, fallback string) string {
236	if v := Get(StylePrefix + key); v != "" {
237		return v
238	}
239	return fallback
240}
241
242// Layout returns the layout slot, or "" when none has been set. Themes
243// substitute their own default in that case.
244func Layout() string { return Get(LayoutSlug) }
245
246// Revision returns the total number of writes this realm has accepted. It
247// changes on every mutation, so a client can cheaply tell "nothing moved".
248func Revision() int { return rev }
249
250// LastHeight returns the block height of the most recent write, or 0.
251func LastHeight() int64 { return lastHeight }
252
253// Manifest returns one tab-separated line per slot:
254//
255//	<slug>\t<rev>\t<bytes>\t<sha256 of body, hex>
256//
257// It is the diff surface scripts/sync.sh reads: one query tells it exactly
258// which local file is out of date, without downloading any body.
259func Manifest() string {
260	var b strings.Builder
261	slots.Iterate("", "", func(key string, value any) bool {
262		s := value.(*Slot)
263		sum := sha256.Sum256([]byte(s.Body))
264		b.WriteString(key)
265		b.WriteString("\t")
266		b.WriteString(strconv.Itoa(s.Rev))
267		b.WriteString("\t")
268		b.WriteString(strconv.Itoa(len(s.Body)))
269		b.WriteString("\t")
270		b.WriteString(hex.EncodeToString(sum[:]))
271		b.WriteString("\n")
272		return false
273	})
274	return b.String()
275}
276
277// Path returns this realm's package path, e.g. "gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home".
278func Path() string { return selfPath }
279
280// Link returns the gnoweb path of a render path on this realm:
281// Link("") is "/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home", Link("slots") is "/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home:slots".
282func Link(sub string) string {
283	p := selfPath
284	if i := strings.Index(p, "/"); i >= 0 {
285		p = p[i:]
286	}
287	if sub == "" {
288		return p
289	}
290	return p + ":" + sub
291}
292
293// ---------------------------------------------------------------------------
294// Themes: the upgradeable part.
295//
296// A theme realm nested under this path (…/home/theme/vN) calls Register from
297// its own init, so deploying it is what nominates it. Nothing serves until the
298// authority runs Accept with that path, which is a separate transaction naming
299// code anyone can go and read. Rollback puts the previous theme back.
300
301// Register records the calling realm's theme as a candidate. The path filed
302// is read off the crossing frame, never taken as an argument, so a candidate
303// cannot claim to live somewhere it does not. Only realms nested under this
304// one are admitted; a direct user call is refused.
305func Register(cur realm, t Theme) { proxy.Propose(0, cur, t) }
306
307// Accept makes the candidate at pkgPath the live theme. Authority only.
308func Accept(cur realm, pkgPath string) { proxy.Accept(0, cur, pkgPath) }
309
310// Withdraw drops a candidate. Authority only, except that a theme realm may
311// always drop its own.
312func Withdraw(cur realm, pkgPath string) { proxy.Withdraw(0, cur, pkgPath) }
313
314// Rollback restores the previous theme and returns the current one to the
315// pending set. Authority only.
316func Rollback(cur realm) { proxy.Rollback(0, cur) }
317
318// Forget drops the rollback history and the storage it holds. Authority only.
319func Forget(cur realm) { proxy.Forget(0, cur) }
320
321// Freeze ends theme upgrades permanently. Slots stay editable. Authority
322// only, and there is no way back.
323func Freeze(cur realm) { proxy.Freeze(0, cur) }
324
325// TransferAuthority hands both editing and upgrading to another address.
326func TransferAuthority(cur realm, to address) {
327	proxy.TransferAuthority(0, cur, upgradeable.NewAddrAuthority(to))
328}
329
330// LivePath returns the realm path of the theme currently serving, or "".
331func LivePath() string { return proxy.LivePath() }
332
333// PendingPaths returns the candidate theme paths awaiting acceptance.
334func PendingPaths() []string {
335	out := []string{}
336	for _, r := range proxy.Pending() {
337		out = append(out, r.PkgPath())
338	}
339	return out
340}
341
342// HistoryPaths returns the themes this realm has already served, oldest
343// first, excluding the live one.
344func HistoryPaths() []string {
345	out := []string{}
346	for _, r := range proxy.History() {
347		out = append(out, r.PkgPath())
348	}
349	return out
350}
351
352// Frozen reports whether theme upgrades have ended.
353func Frozen() bool { return proxy.Frozen() }
354
355// Authority returns a human-readable description of who may write and
356// upgrade.
357func Authority() string { return proxy.Authority().String() }
358
359// liveTheme returns the accepted theme, if any.
360func liveTheme() (Theme, bool) {
361	v, ok := proxy.TryImpl()
362	if !ok {
363		return nil, false
364	}
365	t, ok := v.(Theme)
366	return t, ok
367}