Transaction

FD4170C50BDE92…3C0DE09679AD

Block 240,786 · index 0 · indexed

Summary

Hash
FD4170C50BDE923C544151EED2B50250545457E7598E63DA02813C0DE09679AD
Block
240,786
Size
39572 bytes
Gas used
52,400,392 / 55,020,444
Fee
55021 ugnot
Status
success

Messages

Arguments · 13

  1. #1app
  2. #2app.gno
  3. #3// 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. // // package todo // // import "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/app/v0" // // const Admin = address("g1…") // // var a = app.New(app.Owner(Admin)) // // func init(cur realm) { a.Bind(0, cur) } // func Call(cur realm, verb, payload string) string { return a.Call(0, cur, verb, payload) } // func Register(cur realm, h app.Handler) { a.Register(0, cur, h) } // func Manage(cur realm, verb, arg string) string { return a.Admin(0, cur, verb, arg) } // func Store(_ int, rlm realm) *app.Store { return a.Store(0, rlm) } // func 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. package app import ( "errors" "strconv" "strings" "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0" ) var ( // ErrNoHandler is raised when the app has no accepted implementation. ErrNoHandler = errors.New("app: no handler is live") // ErrNotAHandler is raised when the live implementation does not satisfy // Handler. Register admits only a Handler, so this is unreachable in // practice and cheap to rule out. ErrNotAHandler = errors.New("app: live implementation does not satisfy Handler") // ErrStaleRealm is raised when the threaded realm value is not the // caller's live cur. ErrStaleRealm = errors.New("app: realm value is not the caller's live cur") // ErrNotBound is raised when Bind has not run, so the app does not know // its own path and cannot tell its callers apart. ErrNotBound = errors.New("app: Bind has not been called from init") // ErrForbidden is raised when a realm that is neither the app nor a // registered extension asks for the store. ErrForbidden = errors.New("app: store is not reachable from this realm") // ErrUnknownVerb is raised by Admin for a verb it does not implement. ErrUnknownVerb = errors.New("app: unknown admin verb") // ErrNoSchema is raised when a handler declares no verbs. An app whose // API cannot be enumerated is the thing this package exists to avoid. ErrNoSchema = errors.New("app: a handler must declare at least one verb") ) // 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 Handler interface { // Schema declares the verbs this handler answers. It must not be nil or // empty, and it should be a package-level value rather than rebuilt per // call — the app reads it on every dispatch. Schema() *Schema // Handle answers args.Verb(). Every declared parameter is present and // parses as its kind by the time this runs, so the accessors on Args do // not return errors. Handle(_ int, rlm realm, args *Args) string } // App is the proxy, the state and the dispatch, in one object to hold at // realm level. type App struct { proxy *upgradeable.Proxy data *kv self string } // Owner returns an authority for a single address. Re-exported so a realm // using this package need not import [upgradeable] as well. func Owner(addr address) upgradeable.Authority { return upgradeable.NewAddrAuthority(addr) } // Governed returns an authority for one or more governance realm paths. func Governed(paths ...string) upgradeable.Authority { return upgradeable.NewRealmAuthority(paths...) } // New returns an App whose implementations must be deployed under the app // realm's own path. func New(auth upgradeable.Authority) *App { return &App{proxy: upgradeable.New(auth), data: newKV()} } // NewOpen returns an App that accepts implementations from any realm. The // authority still decides which one serves. func NewOpen(auth upgradeable.Authority) *App { return &App{proxy: upgradeable.NewOpen(auth), data: newKV()} } // Bind teaches the app its own path, which is what every later caller check // compares against. Call it once, from your realm's init: // // func 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 (a *App) Bind(_ int, rlm realm) { if !rlm.IsCurrent() { panic(ErrStaleRealm) } a.self = rlm.PkgPath() } // Proxy exposes the underlying proxy, for realms that want typed entry points // beside Call or their own Render. func (a *App) Proxy() *upgradeable.Proxy { return a.proxy } // The inspection surface. Render shows all of it as a page; these are the // same facts as discrete values, which is what an operator wants from // vm/qeval and what a monitor wants to poll. // LivePath returns the realm path of the handler currently serving, or "". func (a *App) LivePath() string { return a.proxy.LivePath() } // PendingPaths returns the candidate handler paths awaiting acceptance. func (a *App) PendingPaths() []string { return paths(a.proxy.Pending()) } // HistoryPaths returns the handlers already served, oldest first. A non-empty // result is what makes rollback available. func (a *App) HistoryPaths() []string { return paths(a.proxy.History()) } // Extensions returns the realms permitted to act as this app. func (a *App) Extensions() []string { return a.proxy.Extensions() } // Frozen reports whether upgradeability has ended. func (a *App) Frozen() bool { return a.proxy.Frozen() } // Authority describes who may upgrade. func (a *App) Authority() string { return a.proxy.Authority().String() } func paths(rs []upgradeable.Release) []string { out := []string{} for _, r := range rs { out = append(out, r.PkgPath()) } return out } // handler returns the live handler. func (a *App) handler() Handler { v, ok := a.proxy.TryImpl() if !ok { panic(ErrNoHandler) } h, ok := v.(Handler) if !ok { panic(ErrNotAHandler) } return h } // Call 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 (a *App) Call(_ int, rlm realm, verb string, payload string) string { h := a.handler() sc := h.Schema() v, ok := sc.Lookup(verb) if !ok { panic(errors.New("app: unknown verb " + strconv.Quote(verb) + "; this app answers " + strings.Join(sc.Names(), ", "))) } args, err := decode(v, payload) if err != nil { panic(err) } return h.Handle(0, rlm, args) } // Schema returns the live handler's declaration, or nil if nothing is live. func (a *App) Schema() *Schema { v, ok := a.proxy.TryImpl() if !ok { return nil } h, ok := v.(Handler) if !ok { return nil } return h.Schema() } // Verbs enumerates the API. This is the query a caller makes first. func (a *App) Verbs() []string { return a.Schema().Names() } // Signature returns one verb's declaration, or "" if it is not answered. func (a *App) Signature(verb string) string { return a.Schema().Signature(verb) } // SchemaJSON renders the API for tooling: enough to build a form without // reading any Gno source. func (a *App) SchemaJSON() string { return a.Schema().JSON() } // Supports reports whether the live handler answers verb. func (a *App) Supports(verb string) bool { _, ok := a.Schema().Lookup(verb) return ok } // Register 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 (a *App) Register(_ int, rlm realm, h Handler) { if h == nil || len(h.Schema().Names()) == 0 { panic(ErrNoSchema) } a.proxy.Propose(0, rlm, h) } // candidate returns the registered handler at pkgPath. func (a *App) candidate(pkgPath string) (Handler, bool) { for _, r := range a.proxy.Pending() { if r.PkgPath() == pkgPath { h, ok := r.Impl().(Handler) return h, ok } } return nil, false } // accept promotes a candidate, refusing an upgrade that would break callers // unless force is set. // // This is the check a verb-dispatch API cannot have without a schema. Without // it, a handler that quietly drops a verb its predecessor served is accepted // happily and callers find out at runtime. With it, the drop is named before // the upgrade lands. func (a *App) accept(_ int, rlm realm, pkgPath string, force bool) { a.proxy.AssertAuthorized(0, rlm) next, ok := a.candidate(pkgPath) if ok && !force { if breaks := next.Schema().Diff(a.Schema()); len(breaks) > 0 { panic(errors.New("app: refusing a breaking upgrade to " + pkgPath + ": " + strings.Join(breaks, "; ") + " — use the accept-breaking verb to override")) } } a.proxy.Accept(0, rlm, pkgPath) } // Store 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 (a *App) Store(_ int, rlm realm) *Store { a.access(0, rlm) // gate at acquisition, for an immediate, clear refusal return &Store{app: a, rlm: rlm} } // access validates that rlm may reach the data and returns it. It runs at // acquisition and again on every Store operation, so a stale or foreign rlm // is refused every time, not just when the accessor is minted -- which is the // teeth behind Store being a per-call capability. func (a *App) access(_ int, rlm realm) *kv { if !rlm.IsCurrent() { panic(ErrStaleRealm) } if a.self == "" { panic(ErrNotBound) } if c := rlm.PkgPath(); c != a.self && !a.proxy.IsExtension(c) { panic(errors.New(ErrForbidden.Error() + ": " + c)) } return a.data } // Admin is the whole governance surface behind one entry point, so a realm // using this package declares one function instead of one per governance // action. // // accept <path> make a registered candidate live, unless its schema // would break a caller of the one it replaces // accept-breaking <path> // the same, allowing a breaking schema change // withdraw <path> drop a candidate // rollback restore the previous handler // forget drop the rollback history // freeze end upgradeability, permanently // extend <path> let another realm act as this one // unextend <path> revoke that // owner <address> hand authority to an address // govern <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 (a *App) Admin(_ int, rlm realm, verb string, arg string) string { switch verb { case "accept": a.accept(0, rlm, arg, false) case "accept-breaking": a.accept(0, rlm, arg, true) case "withdraw": a.proxy.Withdraw(0, rlm, arg) case "rollback": a.proxy.Rollback(0, rlm) case "forget": a.proxy.Forget(0, rlm) case "freeze": a.proxy.Freeze(0, rlm) case "extend": a.proxy.AddExtension(0, rlm, arg) case "unextend": a.proxy.DropExtension(0, rlm, arg) case "owner": a.proxy.TransferAuthority(0, rlm, Owner(address(arg))) case "govern": parts := strings.Split(arg, ",") paths := make([]string, 0, len(parts)) for _, p := range parts { paths = append(paths, strings.TrimSpace(p)) } a.proxy.TransferAuthority(0, rlm, Governed(paths...)) default: panic(errors.New(ErrUnknownVerb.Error() + ": " + verb)) } return "ok" } // Render 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 (a *App) Render(_ string) string { var b strings.Builder b.WriteString("# " + a.self + "\n\n") if path := a.proxy.LivePath(); path == "" { b.WriteString("_No handler accepted yet._\n\n") } else { b.WriteString("Serving: `" + path + "`\n\n") } b.WriteString("State: **" + strconv.Itoa(a.data.length()) + "** keys\n\n") b.WriteString("Authority: `" + a.proxy.Authority().String() + "`\n\n") if sc := a.Schema(); sc != nil { b.WriteString("## API\n\n" + sc.Markdown() + "\n") } if a.proxy.Frozen() { b.WriteString("**Frozen.** The handler can no longer be replaced.\n\n") } writeList(&b, "Extensions", a.proxy.Extensions()) writeReleases(&b, "Awaiting acceptance", a.proxy.Pending()) writeReleases(&b, "Previously", a.proxy.History()) return b.String() } func writeList(b *strings.Builder, title string, items []string) { if len(items) == 0 { return } b.WriteString("## " + title + "\n\n") for _, s := range items { b.WriteString("- `" + s + "`\n") } b.WriteString("\n") } func writeReleases(b *strings.Builder, title string, rs []upgradeable.Release) { if len(rs) == 0 { return } b.WriteString("## " + title + "\n\n") for _, r := range rs { b.WriteString("- `" + r.PkgPath() + "` (height " + strconv.FormatInt(r.Height(), 10) + ")\n") } b.WriteString("\n") }
  4. #4args.gno
  5. #5package app import ( "encoding/csv" "errors" "strconv" "strings" "unicode/utf8" ) // The wire format is one CSV record. // // CSV because it is in the standard library, because it quotes correctly -- // a task called `buy milk, eggs` survives a round trip, which a naive split // on commas does not -- and because it is writable by hand on a gnokey // command line. Fields are positional, in the order the verb declares them, // which is why the schema forbids a required parameter after an optional one // and why Diff treats a reordering as breaking. // // Encode(addTask, "buy milk, eggs") -> "buy milk, eggs" (quoted by csv) // Encode(transfer, "g1abc…", "42") -> "g1abc…,42" // // A verb with no parameters takes an empty payload. // Args 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. type Args struct { verb Verb values []string } // Verb returns the name of the verb being called. func (a *Args) Verb() string { return a.verb.Name } // Signature returns the declaration this payload was checked against, which // is worth putting in a panic message. func (a *Args) Signature() string { return a.verb.Signature() } // Raw returns the decoded fields in declaration order, for a handler that // would rather loop than name them. func (a *Args) Raw() []string { dup := make([]string, len(a.values)) copy(dup, a.values) return dup } // index returns the position of name and whether a value was supplied. // It panics if the verb has no such parameter: that is the handler asking for // something it did not declare, which no payload can fix. func (a *Args) index(name string, want Kind) (int, bool) { for i, p := range a.verb.Params { if p.Name != name { continue } if want != "" && p.Kind != want { panic(errors.New("app: " + a.verb.Name + ": parameter " + name + " is declared " + string(p.Kind) + ", read as " + string(want))) } return i, i < len(a.values) } panic(errors.New("app: " + a.verb.Name + " has no parameter " + name + "; its signature is " + a.verb.Signature())) } // Has reports whether an optional parameter was supplied. func (a *Args) Has(name string) bool { _, ok := a.index(name, "") return ok } // String returns a string parameter, or "" if it was optional and omitted. func (a *Args) String(name string) string { i, ok := a.index(name, KindString) if !ok { return "" } return a.values[i] } // Int returns an int parameter, or 0 if it was optional and omitted. func (a *Args) Int(name string) int64 { i, ok := a.index(name, KindInt) if !ok { return 0 } n, _ := strconv.ParseInt(a.values[i], 10, 64) // checked at decode return n } // Bool returns a bool parameter, or false if it was optional and omitted. func (a *Args) Bool(name string) bool { i, ok := a.index(name, KindBool) if !ok { return false } return a.values[i] == "true" } // Address returns an address parameter, or the zero address if it was // optional and omitted. func (a *Args) Address(name string) address { i, ok := a.index(name, KindAddress) if !ok { return "" } return address(a.values[i]) } // 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 Encode(v Verb, values ...string) string { if err := checkArity(v, len(values)); err != nil { panic(err) } for i, val := range values { if err := v.Params[i].Kind.parse(val); err != nil { panic(fieldErr(v, i, err)) } } if len(values) == 0 { return "" } // A lone empty field encodes to nothing at all -- csv does not quote an // empty field, so the record "" and the record with no fields are the // same bytes. Quote it explicitly so the payload says which it is. if len(values) == 1 && values[0] == "" { return `""` } // Fast path: when nothing needs quoting, a CSV record IS the fields // joined by commas, and building a csv.Writer to discover that costs // more than the whole rest of the call. See needsQuotes. plain := true for _, val := range values { if needsQuotes(val) { plain = false break } } if plain { return strings.Join(values, ",") } var b strings.Builder w := csv.NewWriter(&b) if err := w.Write(values); err != nil { panic(errors.New("app: " + v.Name + ": " + err.Error())) } w.Flush() if err := w.Error(); err != nil { panic(errors.New("app: " + v.Name + ": " + err.Error())) } return strings.TrimRight(b.String(), "\n") } // splitPlain splits a payload on commas in a single pass, reporting false the // moment it meets a byte that gives commas a different meaning. The one pass // is the point: bailing out and splitting separately doubles the interpreted // work over the whole payload. func splitPlain(payload string) ([]string, bool) { out := []string{} start := 0 for i := 0; i < len(payload); i++ { switch payload[i] { case '"', '\r', '\n': return nil, false case ',': out = append(out, payload[start:i]) start = i + 1 } } return append(out, payload[start:]), true } // needsQuotes mirrors csv.Writer's rule exactly. Diverging from it would mean // Encode produced something csv.Reader reads back differently, so this is // copied rather than approximated: a field is quoted when it is the literal // `\.`, when it contains a comma, quote, CR or LF, or when it begins with a // space. func needsQuotes(field string) bool { if field == "" { return false } if field == `\.` { return true } switch field[0] { case ' ', '\t', '\v', '\f': return true } // A leading non-ASCII space (U+00A0 and friends) is rare enough to hand // to the slow path rather than decode a rune for on every call. if field[0] >= utf8.RuneSelf { return true } // One pass, for the same reason splitPlain is one pass: // strings.ContainsAny is interpreted Gno and costs more over a short // field than the csv.Writer it is trying to avoid. for i := 0; i < len(field); i++ { switch field[i] { case ',', '"', '\r', '\n': return true } } return false } // checkArity reports whether n values can satisfy v. func checkArity(v Verb, n int) error { req := v.Required() if n < req { return errors.New("app: " + v.Name + " needs at least " + strconv.Itoa(req) + " parameter(s), got " + strconv.Itoa(n) + "; signature is " + v.Signature()) } if n > len(v.Params) { return errors.New("app: " + v.Name + " takes at most " + strconv.Itoa(len(v.Params)) + " parameter(s), got " + strconv.Itoa(n) + "; signature is " + v.Signature()) } return nil } // fieldErr names the verb, the parameter and the signature, which is what a // caller needs to fix the payload without reading the handler's source. func fieldErr(v Verb, i int, err error) error { return errors.New("app: " + v.Name + ": parameter " + v.Params[i].Name + ": " + err.Error() + "; signature is " + v.Signature()) } // decode parses and validates a payload against v. func decode(v Verb, payload string) (*Args, error) { values := []string{} if payload == "" { // Ambiguous on its own: no fields, or one empty field. The verb // decides. A required first parameter means an empty payload is that // parameter, empty -- which is what a gnokey caller passing "" for a // string means, and Encode spells `""` for the same case. if v.Required() >= 1 { values = []string{""} } } else if rec, plain := splitPlain(payload); plain { // Fast path: with no quote and no newline anywhere, no field is // quoted, so every comma is a separator and every field is literal // -- exactly what csv.Reader would return. splitPlain decides that // and does the split in one pass, because every string operation // here is interpreted Gno: two passes with strings.ContainsAny and // strings.Split costs more than the csv parse it replaces. values = rec } else { rec, err := csv.NewReader(strings.NewReader(payload)).Read() if err != nil { return nil, errors.New("app: " + v.Name + ": malformed payload: " + err.Error()) } values = rec } if err := checkArity(v, len(values)); err != nil { return nil, err } for i, val := range values { if err := v.Params[i].Kind.parse(val); err != nil { return nil, fieldErr(v, i, err) } } return &Args{verb: v, values: values}, nil }
  6. #6gnomod.toml
  7. #7module = "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/app/v0" gno = "0.9"
  8. #8schema.gno
  9. #9package app import ( "errors" "strconv" "strings" ) // A schema is what a handler says about itself: the verbs it answers, what // each one takes, and what it gives back. Handlers declare one; the app reads // it to enumerate the API, to validate payloads before dispatch, and to refuse // an upgrade that would break a caller. // // This is the part a verb-dispatch API is usually missing. Without it, "the // API" is whatever the live handler's switch statement happens to accept: not // discoverable, not checkable, and free to change under callers without // anything noticing. A schema makes it data, which is the same move that makes // the API growable in the first place. // // What it is not is static typing. Gno has no reflection, so nothing here can // look at your Go types. Validation happens at call time against declarations // you wrote by hand, and a handler whose switch disagrees with its own schema // is a bug the schema cannot catch. // Kind is the type of one payload field. The set is deliberately small: these // are the types that survive a string encoding without ambiguity. type Kind string const ( KindString Kind = "string" KindInt Kind = "int" // int64, base 10 KindBool Kind = "bool" // "true" or "false" KindAddress Kind = "address" // bech32, validated KindNone Kind = "none" // a verb that returns nothing meaningful ) // Valid reports whether k is a known kind. func (k Kind) Valid() bool { switch k { case KindString, KindInt, KindBool, KindAddress, KindNone: return true } return false } // parse checks that v is a well-formed value of kind k. func (k Kind) parse(v string) error { switch k { case KindString, KindNone: return nil case KindInt: if _, err := strconv.ParseInt(v, 10, 64); err != nil { return errors.New("expected an integer, got " + strconv.Quote(v)) } case KindBool: if v != "true" && v != "false" { return errors.New("expected true or false, got " + strconv.Quote(v)) } case KindAddress: if !address(v).IsValid() { return errors.New("expected an address, got " + strconv.Quote(v)) } default: return errors.New("unknown kind " + string(k)) } return nil } // Field is one parameter of a verb. type Field struct { Name string Kind Kind Doc string Opt bool // may be omitted, and only at the end of the parameter list } // P declares a required parameter. func P(name string, kind Kind, doc string) Field { return Field{Name: name, Kind: kind, Doc: doc} } // 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 Opt(name string, kind Kind, doc string) Field { return Field{Name: name, Kind: kind, Doc: doc, Opt: true} } // Verb is one operation's signature. type Verb struct { Name string Doc string Result Kind Params []Field } // Op declares a verb. func Op(name, doc string, result Kind, params ...Field) Verb { return Verb{Name: name, Doc: doc, Result: result, Params: params} } // Required returns how many leading parameters must be supplied. func (v Verb) Required() int { n := 0 for _, p := range v.Params { if p.Opt { break } n++ } return n } // Signature renders the verb as a one-line declaration: // // add(text:string, urgent:bool?) -> int func (v Verb) Signature() string { var b strings.Builder b.WriteString(v.Name) b.WriteString("(") for i, p := range v.Params { if i > 0 { b.WriteString(", ") } b.WriteString(p.Name + ":" + string(p.Kind)) if p.Opt { b.WriteString("?") } } b.WriteString(") -> " + string(v.Result)) return b.String() } // Schema is a handler's whole API, sorted by verb name. type Schema struct { verbs []Verb } // 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 NewSchema(verbs ...Verb) *Schema { s := &Schema{verbs: make([]Verb, 0, len(verbs))} for _, v := range verbs { if strings.TrimSpace(v.Name) == "" || v.Name != strings.TrimSpace(v.Name) { panic(errors.New("app: verb names must be non-blank and unpadded")) } if !v.Result.Valid() { panic(errors.New("app: verb " + v.Name + ": unknown result kind " + string(v.Result))) } if _, dup := s.Lookup(v.Name); dup { panic(errors.New("app: duplicate verb " + v.Name)) } seenOpt := false for i, p := range v.Params { if strings.TrimSpace(p.Name) == "" || p.Name != strings.TrimSpace(p.Name) { panic(errors.New("app: verb " + v.Name + ": parameter names must be non-blank and unpadded")) } if !p.Kind.Valid() || p.Kind == KindNone { panic(errors.New("app: verb " + v.Name + ": parameter " + p.Name + " has unusable kind " + string(p.Kind))) } if p.Opt { seenOpt = true } else if seenOpt { // The encoding is positional, so a required parameter after an // optional one could never be supplied without the optional. panic(errors.New("app: verb " + v.Name + ": required parameter " + p.Name + " follows an optional one")) } for j := 0; j < i; j++ { if v.Params[j].Name == p.Name { panic(errors.New("app: verb " + v.Name + ": duplicate parameter " + p.Name)) } } } at := len(s.verbs) for i, e := range s.verbs { if v.Name < e.Name { at = i break } } s.verbs = append(s.verbs, Verb{}) copy(s.verbs[at+1:], s.verbs[at:]) s.verbs[at] = v } return s } // Names returns the verb names, sorted. func (s *Schema) Names() []string { if s == nil { return nil } out := make([]string, 0, len(s.verbs)) for _, v := range s.verbs { out = append(out, v.Name) } return out } // Lookup returns the verb by name. func (s *Schema) Lookup(name string) (Verb, bool) { if s == nil { return Verb{}, false } for _, v := range s.verbs { if v.Name == name { return v, true } } return Verb{}, false } // Verbs returns a copy of the declarations. func (s *Schema) Verbs() []Verb { if s == nil { return nil } dup := make([]Verb, len(s.verbs)) copy(dup, s.verbs) return dup } // Signature renders one verb, or "" if it is not declared. func (s *Schema) Signature(name string) string { if v, ok := s.Lookup(name); ok { return v.Signature() } return "" } // Markdown renders the API as a table, for Render. func (s *Schema) Markdown() string { if s == nil || len(s.verbs) == 0 { return "_No API declared._\n" } var b strings.Builder b.WriteString("| Verb | Payload | Returns | |\n|---|---|---|---|\n") for _, v := range s.verbs { params := make([]string, 0, len(v.Params)) for _, p := range v.Params { f := p.Name + ":" + string(p.Kind) if p.Opt { f += "?" } params = append(params, f) } p := strings.Join(params, ", ") if p == "" { p = "—" } b.WriteString("| `" + v.Name + "` | `" + p + "` | `" + string(v.Result) + "` | " + v.Doc + " |\n") } return b.String() } // JSON renders the schema for tooling: a client can build a form from this // without reading any Gno source. func (s *Schema) JSON() string { var b strings.Builder b.WriteString(`{"verbs":[`) if s != nil { for i, v := range s.verbs { if i > 0 { b.WriteString(",") } b.WriteString(`{"name":` + jsonStr(v.Name) + `,"doc":` + jsonStr(v.Doc) + `,"result":` + jsonStr(string(v.Result)) + `,"params":[`) for j, p := range v.Params { if j > 0 { b.WriteString(",") } b.WriteString(`{"name":` + jsonStr(p.Name) + `,"kind":` + jsonStr(string(p.Kind)) + `,"doc":` + jsonStr(p.Doc) + `,"optional":` + strconv.FormatBool(p.Opt) + `}`) } b.WriteString(`]}`) } } b.WriteString(`]}`) return b.String() } // Diff 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 (s *Schema) Diff(prev *Schema) []string { out := []string{} if prev == nil { return out } for _, old := range prev.verbs { nv, ok := s.Lookup(old.Name) if !ok { out = append(out, "verb "+old.Name+" removed") continue } if nv.Result != old.Result { out = append(out, "verb "+old.Name+": result changed from "+ string(old.Result)+" to "+string(nv.Result)) } for i, op := range old.Params { if i >= len(nv.Params) { out = append(out, "verb "+old.Name+": parameter "+op.Name+" removed") continue } np := nv.Params[i] if np.Name != op.Name { out = append(out, "verb "+old.Name+": parameter "+strconv.Itoa(i)+ " renamed from "+op.Name+" to "+np.Name) } if np.Kind != op.Kind { out = append(out, "verb "+old.Name+": parameter "+op.Name+ " changed from "+string(op.Kind)+" to "+string(np.Kind)) } if np.Opt != op.Opt && !np.Opt { out = append(out, "verb "+old.Name+": parameter "+op.Name+ " became required") } } for i := len(old.Params); i < len(nv.Params); i++ { if !nv.Params[i].Opt { out = append(out, "verb "+old.Name+": required parameter "+ nv.Params[i].Name+" added") } } } return out } // jsonStr quotes and escapes a string for the JSON rendering above. func jsonStr(s string) string { var b strings.Builder b.WriteString(`"`) for _, r := range s { switch r { case '"': b.WriteString(`\"`) case '\\': b.WriteString(`\\`) case '\n': b.WriteString(`\n`) case '\r': b.WriteString(`\r`) case '\t': b.WriteString(`\t`) default: if r < 0x20 { b.WriteString(`\u00`) const hex = "0123456789abcdef" b.WriteByte(hex[(r>>4)&0xf]) b.WriteByte(hex[r&0xf]) continue } b.WriteRune(r) } } b.WriteString(`"`) return b.String() }
  10. #10store.gno
  11. #11package app import "strconv" // kv is the schema-free key/value data itself. It is unexported and never // handed out: callers reach it only through a [Store], which gates every // operation. Keeping the data and the accessor separate is what lets the // accessor be a revocable capability while the data persists untouched. // // It exists because a struct's fields are as permanent as the realm that // declares them: if your state is `type State struct { count int64 }` in a // realm you can never redeploy, you can never add a second field. Keys are // data, so a kv can grow forever. The trade is everything a type gives you: // nothing is checked, values are strings, and a typo in a key is a silent // empty read. // // # Where the writes land // // A kv is allocated by your realm, so Gno's storage-realm borrow persists // every write in the realm that owns it -- not in the caller's. That is what // lets an implementation realm mutate your state without holding any of it. type kv struct { keys []string // sorted vals []string } func newKV() *kv { return &kv{} } // find returns the insertion index for k and whether k is present. func (s *kv) find(k string) (int, bool) { lo, hi := 0, len(s.keys) for lo < hi { mid := int(uint(lo+hi) >> 1) if s.keys[mid] < k { lo = mid + 1 } else { hi = mid } } return lo, lo < len(s.keys) && s.keys[lo] == k } func (s *kv) get(k string) string { if i, ok := s.find(k); ok { return s.vals[i] } return "" } func (s *kv) has(k string) bool { _, ok := s.find(k) return ok } func (s *kv) set(k, v string) { i, ok := s.find(k) if ok { s.vals[i] = v return } s.keys = append(s.keys, "") s.vals = append(s.vals, "") copy(s.keys[i+1:], s.keys[i:]) copy(s.vals[i+1:], s.vals[i:]) s.keys[i], s.vals[i] = k, v } func (s *kv) del(k string) bool { i, ok := s.find(k) if !ok { return false } s.keys = append(s.keys[:i], s.keys[i+1:]...) s.vals = append(s.vals[:i], s.vals[i+1:]...) return true } func (s *kv) length() int { return len(s.keys) } func (s *kv) allKeys() []string { dup := make([]string, len(s.keys)) copy(dup, s.keys) return dup } func (s *kv) keysWithPrefix(prefix string) []string { out := []string{} i, _ := s.find(prefix) for ; i < len(s.keys); i++ { k := s.keys[i] if len(k) < len(prefix) || k[:len(prefix)] != prefix { break } out = append(out, k) } return out } func (s *kv) getInt(k string) int64 { n, err := strconv.ParseInt(s.get(k), 10, 64) if err != nil { return 0 } return n } func (s *kv) setInt(k string, v int64) { s.set(k, strconv.FormatInt(v, 10)) } func (s *kv) addInt(k string, delta int64) int64 { n := s.getInt(k) + delta s.setInt(k, n) return n } // Store 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]. type Store struct { app *App rlm realm } // Get 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 (s *Store) Get(k string) string { return s.app.access(0, s.rlm).get(k) } // Has reports whether k is present. func (s *Store) Has(k string) bool { return s.app.access(0, s.rlm).has(k) } // Set writes v at k. func (s *Store) Set(k, v string) { s.app.access(0, s.rlm).set(k, v) } // Delete removes k, reporting whether it was there. func (s *Store) Delete(k string) bool { return s.app.access(0, s.rlm).del(k) } // Len returns the number of keys. func (s *Store) Len() int { return s.app.access(0, s.rlm).length() } // Keys returns a copy of the keys, in order. func (s *Store) Keys() []string { return s.app.access(0, s.rlm).allKeys() } // KeysWithPrefix 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 (s *Store) KeysWithPrefix(prefix string) []string { return s.app.access(0, s.rlm).keysWithPrefix(prefix) } // GetInt reads k as an int64, returning 0 if it is absent or unparsable. func (s *Store) GetInt(k string) int64 { return s.app.access(0, s.rlm).getInt(k) } // SetInt writes an int64 at k. func (s *Store) SetInt(k string, v int64) { s.app.access(0, s.rlm).setInt(k, v) } // AddInt adds delta to k and returns the new value. func (s *Store) AddInt(k string, delta int64) int64 { return s.app.access(0, s.rlm).addInt(k, delta) }
  12. #12/gno.MemPackageType
  13. #13 MPUserAll

Result log

msg:0,success:true,log:,events:[]