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

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}