app.gno
14.89 Kb · 423 lines
1// Package app is an upgradeable realm in a handful of forwarding functions.
2//
3// It answers a narrower question than [upgradeable] does. That package gives
4// you a proxy and leaves the shape of your realm to you: your interface, your
5// state, your entry points, all of them permanent. This one makes a different
6// trade — it fixes the shape once, generically, so that nothing about your
7// application is permanent except the path.
8//
9// package todo
10//
11// import "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/app/v0"
12//
13// const Admin = address("g1…")
14//
15// var a = app.New(app.Owner(Admin))
16//
17// func init(cur realm) { a.Bind(0, cur) }
18// func Call(cur realm, verb, payload string) string { return a.Call(0, cur, verb, payload) }
19// func Register(cur realm, h app.Handler) { a.Register(0, cur, h) }
20// func Manage(cur realm, verb, arg string) string { return a.Admin(0, cur, verb, arg) }
21// func Store(_ int, rlm realm) *app.Store { return a.Store(0, rlm) }
22// func Render(path string) string { return a.Render(path) }
23//
24// That realm never needs to change again. Its API is [Handler]'s verb set,
25// which is data; its state is a [Store], whose keys are data. A later
26// implementation adds verbs and keys freely, and callers reach them through
27// the same Call.
28//
29// # Why the forwards cannot be avoided
30//
31// Those six lines are the irreducible minimum. A `p/` package cannot declare
32// crossing functions, and `MsgCall` reaches a function by name at a realm
33// path — so every entry point a user calls has to be declared in your realm.
34// What a pure package can do is make each one a single forward, which is what
35// the methods below are shaped for. Copy the block; only Admin changes.
36//
37// # What you give up
38//
39// Typed entry points. `Call(cur, "transfer", payload)` is one signature
40// forever, so gnoweb shows one function, arguments are strings you encode
41// yourself, and a misspelled verb is a runtime panic rather than a
42// compile-time error. If your API is small and you can name it now, declare it
43// with [upgradeable] and keep your types.
44//
45// The two compose: an app realm can carry hand-written typed entry points
46// beside Call, forwarding to Proxy() for dispatch. Start generic, add typed
47// entry points for the verbs that settle.
48package app
49
50import (
51 "errors"
52 "strconv"
53 "strings"
54
55 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0"
56)
57
58var (
59 // ErrNoHandler is raised when the app has no accepted implementation.
60 ErrNoHandler = errors.New("app: no handler is live")
61
62 // ErrNotAHandler is raised when the live implementation does not satisfy
63 // Handler. Register admits only a Handler, so this is unreachable in
64 // practice and cheap to rule out.
65 ErrNotAHandler = errors.New("app: live implementation does not satisfy Handler")
66
67 // ErrStaleRealm is raised when the threaded realm value is not the
68 // caller's live cur.
69 ErrStaleRealm = errors.New("app: realm value is not the caller's live cur")
70
71 // ErrNotBound is raised when Bind has not run, so the app does not know
72 // its own path and cannot tell its callers apart.
73 ErrNotBound = errors.New("app: Bind has not been called from init")
74
75 // ErrForbidden is raised when a realm that is neither the app nor a
76 // registered extension asks for the store.
77 ErrForbidden = errors.New("app: store is not reachable from this realm")
78
79 // ErrUnknownVerb is raised by Admin for a verb it does not implement.
80 ErrUnknownVerb = errors.New("app: unknown admin verb")
81
82 // ErrNoSchema is raised when a handler declares no verbs. An app whose
83 // API cannot be enumerated is the thing this package exists to avoid.
84 ErrNoSchema = errors.New("app: a handler must declare at least one verb")
85)
86
87// Handler is what an implementation realm implements.
88//
89// Two methods. [Handler.Schema] declares the API as data — a verb set rather
90// than a method set, which is what lets it grow past the realm that serves it.
91// [Handler.Handle] answers one call, having been handed a payload the app has
92// already decoded and checked against that declaration.
93//
94// Panic to fail; panics cross the realm boundary and abort the transaction.
95type Handler interface {
96 // Schema declares the verbs this handler answers. It must not be nil or
97 // empty, and it should be a package-level value rather than rebuilt per
98 // call — the app reads it on every dispatch.
99 Schema() *Schema
100
101 // Handle answers args.Verb(). Every declared parameter is present and
102 // parses as its kind by the time this runs, so the accessors on Args do
103 // not return errors.
104 Handle(_ int, rlm realm, args *Args) string
105}
106
107// App is the proxy, the state and the dispatch, in one object to hold at
108// realm level.
109type App struct {
110 proxy *upgradeable.Proxy
111 data *kv
112 self string
113}
114
115// Owner returns an authority for a single address. Re-exported so a realm
116// using this package need not import [upgradeable] as well.
117func Owner(addr address) upgradeable.Authority { return upgradeable.NewAddrAuthority(addr) }
118
119// Governed returns an authority for one or more governance realm paths.
120func Governed(paths ...string) upgradeable.Authority {
121 return upgradeable.NewRealmAuthority(paths...)
122}
123
124// New returns an App whose implementations must be deployed under the app
125// realm's own path.
126func New(auth upgradeable.Authority) *App {
127 return &App{proxy: upgradeable.New(auth), data: newKV()}
128}
129
130// NewOpen returns an App that accepts implementations from any realm. The
131// authority still decides which one serves.
132func NewOpen(auth upgradeable.Authority) *App {
133 return &App{proxy: upgradeable.NewOpen(auth), data: newKV()}
134}
135
136// Bind teaches the app its own path, which is what every later caller check
137// compares against. Call it once, from your realm's init:
138//
139// func init(cur realm) { a.Bind(0, cur) }
140//
141// It is separate from New because a package-level var initializer runs before
142// there is a realm frame to read the path from.
143func (a *App) Bind(_ int, rlm realm) {
144 if !rlm.IsCurrent() {
145 panic(ErrStaleRealm)
146 }
147 a.self = rlm.PkgPath()
148}
149
150// Proxy exposes the underlying proxy, for realms that want typed entry points
151// beside Call or their own Render.
152func (a *App) Proxy() *upgradeable.Proxy { return a.proxy }
153
154// The inspection surface. Render shows all of it as a page; these are the
155// same facts as discrete values, which is what an operator wants from
156// vm/qeval and what a monitor wants to poll.
157
158// LivePath returns the realm path of the handler currently serving, or "".
159func (a *App) LivePath() string { return a.proxy.LivePath() }
160
161// PendingPaths returns the candidate handler paths awaiting acceptance.
162func (a *App) PendingPaths() []string { return paths(a.proxy.Pending()) }
163
164// HistoryPaths returns the handlers already served, oldest first. A non-empty
165// result is what makes rollback available.
166func (a *App) HistoryPaths() []string { return paths(a.proxy.History()) }
167
168// Extensions returns the realms permitted to act as this app.
169func (a *App) Extensions() []string { return a.proxy.Extensions() }
170
171// Frozen reports whether upgradeability has ended.
172func (a *App) Frozen() bool { return a.proxy.Frozen() }
173
174// Authority describes who may upgrade.
175func (a *App) Authority() string { return a.proxy.Authority().String() }
176
177func paths(rs []upgradeable.Release) []string {
178 out := []string{}
179 for _, r := range rs {
180 out = append(out, r.PkgPath())
181 }
182 return out
183}
184
185// handler returns the live handler.
186func (a *App) handler() Handler {
187 v, ok := a.proxy.TryImpl()
188 if !ok {
189 panic(ErrNoHandler)
190 }
191 h, ok := v.(Handler)
192 if !ok {
193 panic(ErrNotAHandler)
194 }
195 return h
196}
197
198// Call decodes payload against the live handler's declaration for verb, and
199// dispatches.
200//
201// An unknown verb, a payload with the wrong number of fields, or a field that
202// does not parse as its declared kind are all refused here, before the handler
203// runs, with a message naming the verb and its signature.
204func (a *App) Call(_ int, rlm realm, verb string, payload string) string {
205 h := a.handler()
206 sc := h.Schema()
207 v, ok := sc.Lookup(verb)
208 if !ok {
209 panic(errors.New("app: unknown verb " + strconv.Quote(verb) +
210 "; this app answers " + strings.Join(sc.Names(), ", ")))
211 }
212 args, err := decode(v, payload)
213 if err != nil {
214 panic(err)
215 }
216 return h.Handle(0, rlm, args)
217}
218
219// Schema returns the live handler's declaration, or nil if nothing is live.
220func (a *App) Schema() *Schema {
221 v, ok := a.proxy.TryImpl()
222 if !ok {
223 return nil
224 }
225 h, ok := v.(Handler)
226 if !ok {
227 return nil
228 }
229 return h.Schema()
230}
231
232// Verbs enumerates the API. This is the query a caller makes first.
233func (a *App) Verbs() []string { return a.Schema().Names() }
234
235// Signature returns one verb's declaration, or "" if it is not answered.
236func (a *App) Signature(verb string) string { return a.Schema().Signature(verb) }
237
238// SchemaJSON renders the API for tooling: enough to build a form without
239// reading any Gno source.
240func (a *App) SchemaJSON() string { return a.Schema().JSON() }
241
242// Supports reports whether the live handler answers verb.
243func (a *App) Supports(verb string) bool {
244 _, ok := a.Schema().Lookup(verb)
245 return ok
246}
247
248// Register nominates the calling realm's handler. Forward it from your realm
249// and call it from the implementation's init.
250//
251// A handler that declares no verbs is refused here rather than at first call:
252// an app whose API cannot be enumerated is the thing this package exists to
253// avoid.
254func (a *App) Register(_ int, rlm realm, h Handler) {
255 if h == nil || len(h.Schema().Names()) == 0 {
256 panic(ErrNoSchema)
257 }
258 a.proxy.Propose(0, rlm, h)
259}
260
261// candidate returns the registered handler at pkgPath.
262func (a *App) candidate(pkgPath string) (Handler, bool) {
263 for _, r := range a.proxy.Pending() {
264 if r.PkgPath() == pkgPath {
265 h, ok := r.Impl().(Handler)
266 return h, ok
267 }
268 }
269 return nil, false
270}
271
272// accept promotes a candidate, refusing an upgrade that would break callers
273// unless force is set.
274//
275// This is the check a verb-dispatch API cannot have without a schema. Without
276// it, a handler that quietly drops a verb its predecessor served is accepted
277// happily and callers find out at runtime. With it, the drop is named before
278// the upgrade lands.
279func (a *App) accept(_ int, rlm realm, pkgPath string, force bool) {
280 a.proxy.AssertAuthorized(0, rlm)
281 next, ok := a.candidate(pkgPath)
282 if ok && !force {
283 if breaks := next.Schema().Diff(a.Schema()); len(breaks) > 0 {
284 panic(errors.New("app: refusing a breaking upgrade to " + pkgPath +
285 ": " + strings.Join(breaks, "; ") +
286 " — use the accept-breaking verb to override"))
287 }
288 }
289 a.proxy.Accept(0, rlm, pkgPath)
290}
291
292// Store hands out a [Store] accessor to code running on this app's cur: the
293// live handler, and any realm the authority has registered as an extension.
294//
295// A realm holding a perfectly valid cur of its own is refused, because that
296// cur carries its own path. And because the accessor carries the cur and
297// re-checks it on every operation, the grant cannot outlive this call frame:
298// a handler that stashes the accessor aborts the transaction (a realm value
299// cannot be persisted), so rollback and freeze can no longer be undermined by
300// a captured reference. See [Store].
301func (a *App) Store(_ int, rlm realm) *Store {
302 a.access(0, rlm) // gate at acquisition, for an immediate, clear refusal
303 return &Store{app: a, rlm: rlm}
304}
305
306// access validates that rlm may reach the data and returns it. It runs at
307// acquisition and again on every Store operation, so a stale or foreign rlm
308// is refused every time, not just when the accessor is minted -- which is the
309// teeth behind Store being a per-call capability.
310func (a *App) access(_ int, rlm realm) *kv {
311 if !rlm.IsCurrent() {
312 panic(ErrStaleRealm)
313 }
314 if a.self == "" {
315 panic(ErrNotBound)
316 }
317 if c := rlm.PkgPath(); c != a.self && !a.proxy.IsExtension(c) {
318 panic(errors.New(ErrForbidden.Error() + ": " + c))
319 }
320 return a.data
321}
322
323// Admin is the whole governance surface behind one entry point, so a realm
324// using this package declares one function instead of one per governance
325// action.
326//
327// accept <path> make a registered candidate live, unless its schema
328// would break a caller of the one it replaces
329// accept-breaking <path>
330// the same, allowing a breaking schema change
331// withdraw <path> drop a candidate
332// rollback restore the previous handler
333// forget drop the rollback history
334// freeze end upgradeability, permanently
335// extend <path> let another realm act as this one
336// unextend <path> revoke that
337// owner <address> hand authority to an address
338// govern <paths> hand authority to governance realms (comma-separated)
339//
340// Every one of them is authority-gated by the proxy, except that a realm may
341// always withdraw its own candidate. Returns "ok", or panics.
342func (a *App) Admin(_ int, rlm realm, verb string, arg string) string {
343 switch verb {
344 case "accept":
345 a.accept(0, rlm, arg, false)
346 case "accept-breaking":
347 a.accept(0, rlm, arg, true)
348 case "withdraw":
349 a.proxy.Withdraw(0, rlm, arg)
350 case "rollback":
351 a.proxy.Rollback(0, rlm)
352 case "forget":
353 a.proxy.Forget(0, rlm)
354 case "freeze":
355 a.proxy.Freeze(0, rlm)
356 case "extend":
357 a.proxy.AddExtension(0, rlm, arg)
358 case "unextend":
359 a.proxy.DropExtension(0, rlm, arg)
360 case "owner":
361 a.proxy.TransferAuthority(0, rlm, Owner(address(arg)))
362 case "govern":
363 parts := strings.Split(arg, ",")
364 paths := make([]string, 0, len(parts))
365 for _, p := range parts {
366 paths = append(paths, strings.TrimSpace(p))
367 }
368 a.proxy.TransferAuthority(0, rlm, Governed(paths...))
369 default:
370 panic(errors.New(ErrUnknownVerb.Error() + ": " + verb))
371 }
372 return "ok"
373}
374
375// Render is a gnoweb page describing the app's current wiring: what serves,
376// what could, what it used to, who decides, and how much state there is.
377func (a *App) Render(_ string) string {
378 var b strings.Builder
379 b.WriteString("# " + a.self + "\n\n")
380
381 if path := a.proxy.LivePath(); path == "" {
382 b.WriteString("_No handler accepted yet._\n\n")
383 } else {
384 b.WriteString("Serving: `" + path + "`\n\n")
385 }
386 b.WriteString("State: **" + strconv.Itoa(a.data.length()) + "** keys\n\n")
387 b.WriteString("Authority: `" + a.proxy.Authority().String() + "`\n\n")
388
389 if sc := a.Schema(); sc != nil {
390 b.WriteString("## API\n\n" + sc.Markdown() + "\n")
391 }
392
393 if a.proxy.Frozen() {
394 b.WriteString("**Frozen.** The handler can no longer be replaced.\n\n")
395 }
396 writeList(&b, "Extensions", a.proxy.Extensions())
397 writeReleases(&b, "Awaiting acceptance", a.proxy.Pending())
398 writeReleases(&b, "Previously", a.proxy.History())
399 return b.String()
400}
401
402func writeList(b *strings.Builder, title string, items []string) {
403 if len(items) == 0 {
404 return
405 }
406 b.WriteString("## " + title + "\n\n")
407 for _, s := range items {
408 b.WriteString("- `" + s + "`\n")
409 }
410 b.WriteString("\n")
411}
412
413func writeReleases(b *strings.Builder, title string, rs []upgradeable.Release) {
414 if len(rs) == 0 {
415 return
416 }
417 b.WriteString("## " + title + "\n\n")
418 for _, r := range rs {
419 b.WriteString("- `" + r.PkgPath() + "` (height " +
420 strconv.FormatInt(r.Height(), 10) + ")\n")
421 }
422 b.WriteString("\n")
423}