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.

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}