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 gives4// you a proxy and leaves the shape of your realm to you: your interface, your5// state, your entry points, all of them permanent. This one makes a different6// trade — it fixes the shape once, generically, so that nothing about your7// application is permanent except the path.8//9// package todo10//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 later26// implementation adds verbs and keys freely, and callers reach them through27// the same Call.28//29// # Why the forwards cannot be avoided30//31// Those six lines are the irreducible minimum. A `p/` package cannot declare32// crossing functions, and `MsgCall` reaches a function by name at a realm33// 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 what35// the methods below are shaped for. Copy the block; only Admin changes.36//37// # What you give up38//39// Typed entry points. `Call(cur, "transfer", payload)` is one signature40// forever, so gnoweb shows one function, arguments are strings you encode41// yourself, and a misspelled verb is a runtime panic rather than a42// compile-time error. If your API is small and you can name it now, declare it43// with [upgradeable] and keep your types.44//45// The two compose: an app realm can carry hand-written typed entry points46// beside Call, forwarding to Proxy() for dispatch. Start generic, add typed47// entry points for the verbs that settle.48package app4950import (51 "errors"52 "strconv"53 "strings"5455 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0"56)5758var (59 // ErrNoHandler is raised when the app has no accepted implementation.60 ErrNoHandler = errors.New("app: no handler is live")6162 // ErrNotAHandler is raised when the live implementation does not satisfy63 // Handler. Register admits only a Handler, so this is unreachable in64 // practice and cheap to rule out.65 ErrNotAHandler = errors.New("app: live implementation does not satisfy Handler")6667 // ErrStaleRealm is raised when the threaded realm value is not the68 // caller's live cur.69 ErrStaleRealm = errors.New("app: realm value is not the caller's live cur")7071 // ErrNotBound is raised when Bind has not run, so the app does not know72 // its own path and cannot tell its callers apart.73 ErrNotBound = errors.New("app: Bind has not been called from init")7475 // ErrForbidden is raised when a realm that is neither the app nor a76 // registered extension asks for the store.77 ErrForbidden = errors.New("app: store is not reachable from this realm")7879 // ErrUnknownVerb is raised by Admin for a verb it does not implement.80 ErrUnknownVerb = errors.New("app: unknown admin verb")8182 // ErrNoSchema is raised when a handler declares no verbs. An app whose83 // 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)8687// Handler is what an implementation realm implements.88//89// Two methods. [Handler.Schema] declares the API as data — a verb set rather90// 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 has92// 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 or97 // empty, and it should be a package-level value rather than rebuilt per98 // call — the app reads it on every dispatch.99 Schema() *Schema100101 // Handle answers args.Verb(). Every declared parameter is present and102 // parses as its kind by the time this runs, so the accessors on Args do103 // not return errors.104 Handle(_ int, rlm realm, args *Args) string105}106107// App is the proxy, the state and the dispatch, in one object to hold at108// realm level.109type App struct {110 proxy *upgradeable.Proxy111 data *kv112 self string113}114115// Owner returns an authority for a single address. Re-exported so a realm116// using this package need not import [upgradeable] as well.117func Owner(addr address) upgradeable.Authority { return upgradeable.NewAddrAuthority(addr) }118119// Governed returns an authority for one or more governance realm paths.120func Governed(paths ...string) upgradeable.Authority {121 return upgradeable.NewRealmAuthority(paths...)122}123124// New returns an App whose implementations must be deployed under the app125// realm's own path.126func New(auth upgradeable.Authority) *App {127 return &App{proxy: upgradeable.New(auth), data: newKV()}128}129130// NewOpen returns an App that accepts implementations from any realm. The131// authority still decides which one serves.132func NewOpen(auth upgradeable.Authority) *App {133 return &App{proxy: upgradeable.NewOpen(auth), data: newKV()}134}135136// Bind teaches the app its own path, which is what every later caller check137// 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 before142// 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}149150// Proxy exposes the underlying proxy, for realms that want typed entry points151// beside Call or their own Render.152func (a *App) Proxy() *upgradeable.Proxy { return a.proxy }153154// The inspection surface. Render shows all of it as a page; these are the155// same facts as discrete values, which is what an operator wants from156// vm/qeval and what a monitor wants to poll.157158// LivePath returns the realm path of the handler currently serving, or "".159func (a *App) LivePath() string { return a.proxy.LivePath() }160161// PendingPaths returns the candidate handler paths awaiting acceptance.162func (a *App) PendingPaths() []string { return paths(a.proxy.Pending()) }163164// HistoryPaths returns the handlers already served, oldest first. A non-empty165// result is what makes rollback available.166func (a *App) HistoryPaths() []string { return paths(a.proxy.History()) }167168// Extensions returns the realms permitted to act as this app.169func (a *App) Extensions() []string { return a.proxy.Extensions() }170171// Frozen reports whether upgradeability has ended.172func (a *App) Frozen() bool { return a.proxy.Frozen() }173174// Authority describes who may upgrade.175func (a *App) Authority() string { return a.proxy.Authority().String() }176177func paths(rs []upgradeable.Release) []string {178 out := []string{}179 for _, r := range rs {180 out = append(out, r.PkgPath())181 }182 return out183}184185// 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 h196}197198// Call decodes payload against the live handler's declaration for verb, and199// dispatches.200//201// An unknown verb, a payload with the wrong number of fields, or a field that202// does not parse as its declared kind are all refused here, before the handler203// 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}218219// 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 nil224 }225 h, ok := v.(Handler)226 if !ok {227 return nil228 }229 return h.Schema()230}231232// Verbs enumerates the API. This is the query a caller makes first.233func (a *App) Verbs() []string { return a.Schema().Names() }234235// Signature returns one verb's declaration, or "" if it is not answered.236func (a *App) Signature(verb string) string { return a.Schema().Signature(verb) }237238// SchemaJSON renders the API for tooling: enough to build a form without239// reading any Gno source.240func (a *App) SchemaJSON() string { return a.Schema().JSON() }241242// Supports reports whether the live handler answers verb.243func (a *App) Supports(verb string) bool {244 _, ok := a.Schema().Lookup(verb)245 return ok246}247248// Register nominates the calling realm's handler. Forward it from your realm249// 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 to253// 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}260261// 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, ok267 }268 }269 return nil, false270}271272// accept promotes a candidate, refusing an upgrade that would break callers273// unless force is set.274//275// This is the check a verb-dispatch API cannot have without a schema. Without276// it, a handler that quietly drops a verb its predecessor served is accepted277// happily and callers find out at runtime. With it, the drop is named before278// 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}291292// Store hands out a [Store] accessor to code running on this app's cur: the293// 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 that296// cur carries its own path. And because the accessor carries the cur and297// 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 value299// cannot be persisted), so rollback and freeze can no longer be undermined by300// 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 refusal303 return &Store{app: a, rlm: rlm}304}305306// access validates that rlm may reach the data and returns it. It runs at307// acquisition and again on every Store operation, so a stale or foreign rlm308// is refused every time, not just when the accessor is minted -- which is the309// 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.data321}322323// Admin is the whole governance surface behind one entry point, so a realm324// using this package declares one function instead of one per governance325// action.326//327// accept <path> make a registered candidate live, unless its schema328// would break a caller of the one it replaces329// accept-breaking <path>330// the same, allowing a breaking schema change331// withdraw <path> drop a candidate332// rollback restore the previous handler333// forget drop the rollback history334// freeze end upgradeability, permanently335// extend <path> let another realm act as this one336// unextend <path> revoke that337// owner <address> hand authority to an address338// 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 may341// 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}374375// 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.Builder379 b.WriteString("# " + a.self + "\n\n")380381 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")388389 if sc := a.Schema(); sc != nil {390 b.WriteString("## API\n\n" + sc.Markdown() + "\n")391 }392393 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}401402func writeList(b *strings.Builder, title string, items []string) {403 if len(items) == 0 {404 return405 }406 b.WriteString("## " + title + "\n\n")407 for _, s := range items {408 b.WriteString("- `" + s + "`\n")409 }410 b.WriteString("\n")411}412413func writeReleases(b *strings.Builder, title string, rs []upgradeable.Release) {414 if len(rs) == 0 {415 return416 }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}424Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.