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}