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.

settings.gno

10.87 Kb · 335 lines
  1package config
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"chain"
  8	"chain/runtime"
  9
 10	"gno.land/p/nym-moultest123/mygnoscan/v0"
 11	"gno.land/p/nym-moultest123/pausable/v0"
 12	"gno.land/p/nt/avl/v0"
 13)
 14
 15// The keys this realm answers for by name. A key is just a string and any key
 16// can be set, but one the rest of the repo reads gets a constant here so the
 17// reader and the writer cannot drift apart on a typo.
 18const (
 19	// KeyMygnoscanURL is the explorer instance every realm of moul's links to.
 20	KeyMygnoscanURL = "mygnoscan.url"
 21	// KeyMygnoscanNetwork overrides the ?network= id, for an instance that
 22	// names the chain something other than what mygnoscan.NetworkFor expects.
 23	// Unset is the normal case: the chain-id decides.
 24	KeyMygnoscanNetwork = "mygnoscan.network"
 25)
 26
 27// maxValueLen bounds one write. Storage is paid for and never refunded, so
 28// this realm holds settings, not content: a URL, a flag, a short list.
 29//
 30// A notice block is the one that pushes against it, and 1024 bytes is several
 31// paragraphs of markdown. A realm that needs more than that is asking this one
 32// to be a CMS, which is what its own slots are for.
 33const maxValueLen = 1024
 34
 35type setting struct {
 36	value   string
 37	rev     int
 38	updated int64
 39}
 40
 41var (
 42	settings    = avl.NewTree() // key -> *setting
 43	settingsRev int
 44)
 45
 46// Set creates or replaces a setting. Only an address the Authorizer accepts
 47// may call it.
 48//
 49// It panics rather than returning an error, which is the opposite of the
 50// manager functions in config.gno, and deliberately so: a returned error from
 51// a realm call leaves the transaction SUCCESSFUL. A rejected config write that
 52// reports success is the one outcome worth ruling out here, because the caller
 53// then walks away believing the new value is live and every realm reading this
 54// key keeps serving the old one. The manager functions predate that reasoning
 55// and are frozen on chain at v0; new surface does not inherit the mistake.
 56func Set(cur realm, key, value string) {
 57	assertWritableKey(key)
 58	assertValue(key, value)
 59	assertAuthorized(cur, "config.Set:"+key)
 60	writeSetting(key, value)
 61}
 62
 63// Unset removes a setting, so readers fall back to their built-in default.
 64// Removing a key that is not set is an error, not a no-op: it almost always
 65// means the key was misspelled here or in the realm that reads it.
 66func Unset(cur realm, key string) {
 67	assertWritableKey(key)
 68	assertAuthorized(cur, "config.Unset:"+key)
 69	removeSetting(key)
 70}
 71
 72// writeSetting is the only place a setting is stored, shared by the direct and
 73// the relayed path so a new caller cannot forget the revision or the event.
 74func writeSetting(key, value string) {
 75	settingsRev++
 76	settings.Set(key, &setting{
 77		value:   value,
 78		rev:     settingsRev,
 79		updated: runtime.ChainHeight(),
 80	})
 81	chain.Emit("ConfigSet", "key", key, "value", value, "rev", strconv.Itoa(settingsRev))
 82}
 83
 84// removeSetting is writeSetting's twin.
 85func removeSetting(key string) {
 86	if _, removed := settings.Remove(key); !removed {
 87		panic("no such setting: " + key)
 88	}
 89	settingsRev++
 90	chain.Emit("ConfigUnset", "key", key, "rev", strconv.Itoa(settingsRev))
 91}
 92
 93func assertWritableKey(key string) {
 94	if !validKey(key) {
 95		panic("invalid key: want <name> or <name>" + ScopeSep + "<realm-path>, " +
 96			"name being 1-" + strconv.Itoa(maxNameLen) + " bytes of [a-z0-9._-], got " +
 97			strconv.Quote(key))
 98	}
 99}
100
101// assertValue rejects a value the reader could not make sense of.
102//
103// Only pause has a grammar today, and it is exactly the setting where a
104// silently unreadable value matters: pausable.MustParse fails CLOSED, so a
105// typo stored here would take every realm offline at the next render. Catching
106// it on the way in costs one comparison and turns that into a failed
107// transaction the writer sees immediately.
108func assertValue(key, value string) {
109	if len(value) > maxValueLen {
110		panic("value too long: max " + strconv.Itoa(maxValueLen) +
111			" bytes, got " + strconv.Itoa(len(value)))
112	}
113	if name, _ := SplitKey(key); name == KeyPause {
114		if _, ok := pausable.Parse(value); !ok {
115			panic("invalid pause value " + strconv.Quote(value) +
116				": want \"\", \"running\", \"readonly\" or \"paused\", each optionally followed by \": <reason>\"")
117		}
118	}
119}
120
121// assertAuthorized runs the write through the Authorizer, so whoever holds
122// authority today decides: the member list at first, a DAO or a contract after
123// a TransferManagement. The action closure is what authz authorizes; the write
124// itself happens after, because a nil error is the only thing that reaches it.
125func assertAuthorized(cur realm, title string) {
126	allowed := false
127	err := Authorizer.DoByPrevious(0, cur, title, func() error {
128		allowed = true
129		return nil
130	})
131	if err != nil {
132		panic("unauthorized: " + err.Error())
133	}
134	if !allowed {
135		panic("unauthorized")
136	}
137}
138
139// Get returns a setting's value, or "" when it is not set. Reads are open: a
140// realm importing this one calls Get on every render and pays nothing beyond
141// the cross-realm call.
142func Get(key string) string {
143	v := settings.Get(key)
144	if v == nil {
145		return ""
146	}
147	return v.(*setting).value
148}
149
150// GetOr returns a setting's value, or fallback when it is unset or empty.
151// This is the shape every consumer wants: a realm should still render on a
152// chain where this one was never deployed with that key.
153func GetOr(key, fallback string) string {
154	if v := Get(key); v != "" {
155		return v
156	}
157	return fallback
158}
159
160// Has reports whether key is set, including to the empty string, which GetOr
161// cannot distinguish from unset.
162func Has(key string) bool { return settings.Has(key) }
163
164// Keys returns every set key, in sorted order.
165func Keys() []string {
166	out := []string{}
167	settings.Iterate("", "", func(key string, _ any) bool {
168		out = append(out, key)
169		return false
170	})
171	return out
172}
173
174// Size returns how many settings are set.
175func Size() int { return settings.Size() }
176
177// SettingsRevision counts every settings write this realm has accepted. A
178// client that caches config polls this one int to learn that nothing moved.
179func SettingsRevision() int { return settingsRev }
180
181// Manifest returns one tab-separated line per setting:
182//
183//	<key>\t<rev>\t<height>\t<value>
184//
185// It is the whole config in one vm/qeval read, for a tool that wants to diff
186// the chain against a local file without a call per key. The value is last
187// because it is the only field that can contain a tab.
188func Manifest() string {
189	var b strings.Builder
190	settings.Iterate("", "", func(key string, value any) bool {
191		s := value.(*setting)
192		b.WriteString(key)
193		b.WriteString("\t")
194		b.WriteString(strconv.Itoa(s.rev))
195		b.WriteString("\t")
196		b.WriteString(strconv.FormatInt(s.updated, 10))
197		b.WriteString("\t")
198		b.WriteString(s.value)
199		b.WriteString("\n")
200		return false
201	})
202	return b.String()
203}
204
205// MygnoscanURL returns the explorer base moul's realms should link to, falling
206// back to the package default when nothing is set here.
207func MygnoscanURL() string { return GetOr(KeyMygnoscanURL, mygnoscan.DefaultBase) }
208
209// Scanner returns a link builder pointed at the configured explorer, on
210// whichever network answers for the running chain (or at the KeyMygnoscanNetwork
211// override, when one is set).
212//
213// This is the function other realms are meant to call:
214//
215//	config.Scanner().Realm("gno.land/r/nym-moultest123/home")
216//	config.Scanner().Address(someAddr)
217//
218// Changing where every one of them points is then one transaction against this
219// realm, not a redeploy of each.
220func Scanner() mygnoscan.Scanner {
221	s := mygnoscan.New(MygnoscanURL())
222	if net := Get(KeyMygnoscanNetwork); net != "" {
223		s = s.WithNetwork(net)
224	}
225	return s
226}
227
228// Mygnoscan is the explorer page of the realm calling in.
229func Mygnoscan() string { return MygnoscanFor(caller()) }
230
231// MygnoscanFor is Mygnoscan for a named realm.
232func MygnoscanFor(pkgPath string) string { return Scanner().Realm(pkgPath) }
233
234// MygnoscanFooter is the render-ready form: the markdown line the realm
235// calling in appends under its output.
236func MygnoscanFooter() string { return MygnoscanFooterFor(caller()) }
237
238// MygnoscanFooterFor is MygnoscanFooter for a named realm.
239func MygnoscanFooterFor(pkgPath string) string { return Scanner().RealmFooter(pkgPath) }
240
241// Render shows the whole configuration: the settings, who may change them, and
242// where this realm itself can be inspected.
243func Render(path string) string {
244	var b strings.Builder
245
246	b.WriteString("# gno.land/r/nym-moultest123/config\n\n")
247	b.WriteString("moul's settings, read by his other realms. ")
248	b.WriteString("rev ")
249	b.WriteString(strconv.Itoa(settingsRev))
250	b.WriteString(" · ")
251	b.WriteString(strconv.Itoa(settings.Size()))
252	b.WriteString(" setting(s)\n\n")
253
254	b.WriteString("## Settings\n\n")
255	if settings.Size() == 0 {
256		b.WriteString("_none set; every reader is on its built-in default_\n")
257	} else {
258		b.WriteString("| key | value | rev | height |\n")
259		b.WriteString("| --- | --- | ---: | ---: |\n")
260		settings.Iterate("", "", func(key string, value any) bool {
261			s := value.(*setting)
262			b.WriteString("| `")
263			b.WriteString(key)
264			b.WriteString("` | ")
265			b.WriteString(renderValue(s.value))
266			b.WriteString(" | ")
267			b.WriteString(strconv.Itoa(s.rev))
268			b.WriteString(" | ")
269			b.WriteString(strconv.FormatInt(s.updated, 10))
270			b.WriteString(" |\n")
271			return false
272		})
273	}
274
275	b.WriteString("\n## Pause\n\n")
276	b.WriteString(renderPause())
277
278	b.WriteString("\n## Authority\n\n")
279	b.WriteString("`")
280	b.WriteString(Authorizer.String())
281	b.WriteString("`\n")
282
283	b.WriteString(renderProxies())
284
285	b.WriteString("\n---\n\n")
286	b.WriteString(MygnoscanFooterFor(realmPath))
287	b.WriteString("\n")
288
289	return b.String()
290}
291
292// renderPause reports the global pause only. A per-realm one is a setting like
293// any other and is already in the table above; repeating them here would be a
294// second list to keep in step, and the one a reader of THIS page wants is the
295// switch that covers everything.
296func renderPause() string {
297	st := pausable.MustParse(Get(KeyPause))
298	if !st.IsPaused() {
299		return "_running; no global pause_\n"
300	}
301	return st.Notice() + "\n"
302}
303
304// renderProxies lists the later versions allowed to relay writes here. The
305// section is omitted entirely while there are none, which is the state this
306// realm ships in and stays in until a v2 exists.
307func renderProxies() string {
308	paths := ListProxies()
309	if len(paths) == 0 {
310		return ""
311	}
312	var b strings.Builder
313	b.WriteString("\n## Proxies\n\n")
314	b.WriteString("Later versions of this realm that may relay a write:\n\n")
315	for _, p := range paths {
316		b.WriteString("- `")
317		b.WriteString(p)
318		b.WriteString("`\n")
319	}
320	return b.String()
321}
322
323// renderValue puts a value in a code span and neutralises the two characters
324// that would otherwise break out of the table cell it sits in. Only a manager
325// can write a value, so this is about a stray pipe in a URL and not about an
326// attacker, but a config page that silently loses a column is its own bug.
327func renderValue(v string) string {
328	if v == "" {
329		return "_(empty)_"
330	}
331	v = strings.ReplaceAll(v, "|", "\\|")
332	v = strings.ReplaceAll(v, "`", "'")
333	v = strings.ReplaceAll(v, "\n", " ")
334	return "`" + v + "`"
335}