v0 source pure
Package app is an upgradeable realm in a handful of forwarding functions.
Package app is an upgradeable realm in a handful of forwarding functions.
It answers a narrower question than upgradeable does. That package gives you a proxy and leaves the shape of your realm to you: your interface, your state, your entry points, all of them permanent. This one makes a different trade — it fixes the shape once, generically, so that nothing about your application is permanent except the path.
Example
1package todo
2
3import "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/app/v0"
4
5const Admin = address("g1…")
6
7var a = app.New(app.Owner(Admin))
8
9func init(cur realm) { a.Bind(0, cur) }
10func Call(cur realm, verb, payload string) string { return a.Call(0, cur, verb, payload) }
11func Register(cur realm, h app.Handler) { a.Register(0, cur, h) }
12func Manage(cur realm, verb, arg string) string { return a.Admin(0, cur, verb, arg) }
13func Store(_ int, rlm realm) *app.Store { return a.Store(0, rlm) }
14func Render(path string) string { return a.Render(path) }
That realm never needs to change again. Its API is Handler's verb set, which is data; its state is a Store, whose keys are data. A later implementation adds verbs and keys freely, and callers reach them through the same Call.
Why the forwards cannot be avoided
Those six lines are the irreducible minimum. A `p/` package cannot declare crossing functions, and `MsgCall` reaches a function by name at a realm path — so every entry point a user calls has to be declared in your realm. What a pure package can do is make each one a single forward, which is what the methods below are shaped for. Copy the block; only Admin changes.
What you give up
Typed entry points. `Call(cur, "transfer", payload)` is one signature forever, so gnoweb shows one function, arguments are strings you encode yourself, and a misspelled verb is a runtime panic rather than a compile-time error. If your API is small and you can name it now, declare it with upgradeable and keep your types.
The two compose: an app realm can carry hand-written typed entry points beside Call, forwarding to Proxy() for dispatch. Start generic, add typed entry points for the verbs that settle.
1
1
var ErrNoHandler, ErrNotAHandler, ErrStaleRealm, ErrNotBound, ErrForbidden, ErrUnknownVerb, ErrNoSchema
1var (
2 // ErrNoHandler is raised when the app has no accepted implementation.
3 ErrNoHandler = errors.New("app: no handler is live")
4
5 // ErrNotAHandler is raised when the live implementation does not satisfy
6 // Handler. Register admits only a Handler, so this is unreachable in
7 // practice and cheap to rule out.
8 ErrNotAHandler = errors.New("app: live implementation does not satisfy Handler")
9
10 // ErrStaleRealm is raised when the threaded realm value is not the
11 // caller's live cur.
12 ErrStaleRealm = errors.New("app: realm value is not the caller's live cur")
13
14 // ErrNotBound is raised when Bind has not run, so the app does not know
15 // its own path and cannot tell its callers apart.
16 ErrNotBound = errors.New("app: Bind has not been called from init")
17
18 // ErrForbidden is raised when a realm that is neither the app nor a
19 // registered extension asks for the store.
20 ErrForbidden = errors.New("app: store is not reachable from this realm")
21
22 // ErrUnknownVerb is raised by Admin for a verb it does not implement.
23 ErrUnknownVerb = errors.New("app: unknown admin verb")
24
25 // ErrNoSchema is raised when a handler declares no verbs. An app whose
26 // API cannot be enumerated is the thing this package exists to avoid.
27 ErrNoSchema = errors.New("app: a handler must declare at least one verb")
28)9
func Encode
Encode builds a payload for v from values given in declaration order, validating each against its declared kind. Use it when one realm calls another app, so an encoding mistake fails where it was made.
func Governed
Governed returns an authority for one or more governance realm paths.
func Owner
Owner returns an authority for a single address. Re-exported so a realm using this package need not import upgradeable as well.
func New
New returns an App whose implementations must be deployed under the app realm's own path.
func NewOpen
NewOpen returns an App that accepts implementations from any realm. The authority still decides which one serves.
func Opt
Opt declares an optional parameter. Optional parameters must come last, and appending one is the only way to add a parameter to a verb without breaking callers.
func P
P declares a required parameter.
func NewSchema
NewSchema validates and sorts the declarations. It panics on a schema that could not be served correctly: a blank or duplicate name, an unknown kind, a required parameter after an optional one, or a duplicate parameter name. Those are all authoring mistakes, and a handler carrying one should fail to deploy rather than mis-describe itself forever.
func Op
Op declares a verb.
8
type App
structApp is the proxy, the state and the dispatch, in one object to hold at realm level.
Methods on App
func Admin
method on AppAdmin is the whole governance surface behind one entry point, so a realm using this package declares one function instead of one per governance action.
Example
1accept <path> make a registered candidate live, unless its schema
2 would break a caller of the one it replaces
3accept-breaking <path>
4 the same, allowing a breaking schema change
5withdraw <path> drop a candidate
6rollback restore the previous handler
7forget drop the rollback history
8freeze end upgradeability, permanently
9extend <path> let another realm act as this one
10unextend <path> revoke that
11owner <address> hand authority to an address
12govern <paths> hand authority to governance realms (comma-separated)
Every one of them is authority-gated by the proxy, except that a realm may always withdraw its own candidate. Returns "ok", or panics.
func Authority
method on AppAuthority describes who may upgrade.
func Bind
method on AppBind teaches the app its own path, which is what every later caller check compares against. Call it once, from your realm's init:
Example
1func init(cur realm) { a.Bind(0, cur) }
It is separate from New because a package-level var initializer runs before there is a realm frame to read the path from.
func Call
method on AppCall decodes payload against the live handler's declaration for verb, and dispatches.
An unknown verb, a payload with the wrong number of fields, or a field that does not parse as its declared kind are all refused here, before the handler runs, with a message naming the verb and its signature.
func Extensions
method on AppExtensions returns the realms permitted to act as this app.
func Frozen
method on AppFrozen reports whether upgradeability has ended.
func HistoryPaths
method on AppHistoryPaths returns the handlers already served, oldest first. A non-empty result is what makes rollback available.
func LivePath
method on AppLivePath returns the realm path of the handler currently serving, or "".
func PendingPaths
method on AppPendingPaths returns the candidate handler paths awaiting acceptance.
func Proxy
method on AppProxy exposes the underlying proxy, for realms that want typed entry points beside Call or their own Render.
func Register
method on AppRegister nominates the calling realm's handler. Forward it from your realm and call it from the implementation's init.
A handler that declares no verbs is refused here rather than at first call: an app whose API cannot be enumerated is the thing this package exists to avoid.
func Render
method on AppRender is a gnoweb page describing the app's current wiring: what serves, what could, what it used to, who decides, and how much state there is.
func Schema
method on AppSchema returns the live handler's declaration, or nil if nothing is live.
func SchemaJSON
method on AppSchemaJSON renders the API for tooling: enough to build a form without reading any Gno source.
func Signature
method on AppSignature returns one verb's declaration, or "" if it is not answered.
func Store
method on AppStore hands out a Store accessor to code running on this app's cur: the live handler, and any realm the authority has registered as an extension.
A realm holding a perfectly valid cur of its own is refused, because that cur carries its own path. And because the accessor carries the cur and re-checks it on every operation, the grant cannot outlive this call frame: a handler that stashes the accessor aborts the transaction (a realm value cannot be persisted), so rollback and freeze can no longer be undermined by a captured reference. See Store.
func Supports
method on AppSupports reports whether the live handler answers verb.
func Verbs
method on AppVerbs enumerates the API. This is the query a caller makes first.
type Args
structArgs is a decoded, validated payload. A handler receives one and reads fields by name; the app has already checked that every field is present and parses as its declared kind, so the accessors do not return errors.
Methods on Args
func Address
method on ArgsAddress returns an address parameter, or the zero address if it was optional and omitted.
func Bool
method on ArgsBool returns a bool parameter, or false if it was optional and omitted.
func Has
method on ArgsHas reports whether an optional parameter was supplied.
func Int
method on ArgsInt returns an int parameter, or 0 if it was optional and omitted.
func Raw
method on ArgsRaw returns the decoded fields in declaration order, for a handler that would rather loop than name them.
func Signature
method on ArgsSignature returns the declaration this payload was checked against, which is worth putting in a panic message.
func String
method on ArgsString returns a string parameter, or "" if it was optional and omitted.
func Verb
method on ArgsVerb returns the name of the verb being called.
type Field
structField is one parameter of a verb.
type Handler
interface 1type Handler interface {
2 // Schema declares the verbs this handler answers. It must not be nil or
3 // empty, and it should be a package-level value rather than rebuilt per
4 // call — the app reads it on every dispatch.
5 Schema() *Schema
6
7 // Handle answers args.Verb(). Every declared parameter is present and
8 // parses as its kind by the time this runs, so the accessors on Args do
9 // not return errors.
10 Handle(_ int, rlm realm, args *Args) string
11}Handler is what an implementation realm implements.
Two methods. [Handler.Schema] declares the API as data — a verb set rather than a method set, which is what lets it grow past the realm that serves it. [Handler.Handle] answers one call, having been handed a payload the app has already decoded and checked against that declaration.
Panic to fail; panics cross the realm boundary and abort the transaction.
type Kind
identKind is the type of one payload field. The set is deliberately small: these are the types that survive a string encoding without ambiguity.
type Schema
structSchema is a handler's whole API, sorted by verb name.
Methods on Schema
func Diff
method on SchemaDiff reports what upgrading from prev to s would break for existing callers. An empty result means every payload that worked before still works.
Breaking: a verb disappears, a parameter disappears or changes kind or position, a new required parameter appears, or a result kind changes. Not breaking: a new verb, a new optional parameter at the end, new documentation.
func JSON
method on SchemaJSON renders the schema for tooling: a client can build a form from this without reading any Gno source.
func Lookup
method on SchemaLookup returns the verb by name.
func Markdown
method on SchemaMarkdown renders the API as a table, for Render.
func Names
method on SchemaNames returns the verb names, sorted.
func Signature
method on SchemaSignature renders one verb, or "" if it is not declared.
func Verbs
method on SchemaVerbs returns a copy of the declarations.
type Store
structStore is the handler-facing accessor. It is NOT the data -- it is a permission to touch it, bound to the realm value it was issued for.
This is what closes the capability leak. The old design handed out a raw pointer to the data, which a handler could stash in a package variable and keep using after it was rolled back or the realm was frozen. A Store holds the caller's realm value and re-checks it on every operation, so:
- It cannot be stashed. Assigning it to realm state persists the realm value it carries, which the VM refuses ("cannot persist realm value"), aborting the transaction. The capability cannot outlive the call.
- It cannot be replayed. Even held transiently, a Store whose frame has returned fails IsCurrent() on its next use.
So the grant lives and dies with the exact call frame that obtained it, which is the only window in which the caller is genuinely the live handler (or a registered extension). See App.Store.
Methods on Store
func AddInt
method on StoreAddInt adds delta to k and returns the new value.
func Delete
method on StoreDelete removes k, reporting whether it was there.
func Get
method on StoreGet returns the value at k, or "" if absent. An absent key and a key set to "" read the same; use Has to tell them apart.
func GetInt
method on StoreGetInt reads k as an int64, returning 0 if it is absent or unparsable.
func Has
method on StoreHas reports whether k is present.
func Keys
method on StoreKeys returns a copy of the keys, in order.
func KeysWithPrefix
method on StoreKeysWithPrefix returns the keys under a prefix, in order. Prefixes are how a Store holds more than one collection: "task/1", "task/2", "meta/owner".
func Len
method on StoreLen returns the number of keys.
func Set
method on StoreSet writes v at k.
func SetInt
method on StoreSetInt writes an int64 at k.
type Verb
structVerb is one operation's signature.
6
- encoding/csv stdlib
- errors stdlib
- gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0 package
- strconv stdlib
- strings stdlib
- unicode/utf8 stdlib