Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

settings.gno

10.83 Kb · 335 lines
  1package config
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"chain"
  8	"chain/runtime"
  9
 10	"gno.land/p/moul/mygnoscan/v0"
 11	"gno.land/p/moul/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/moul/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/moul/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}