1// Package hubv2 is the multi-admin module router for gnomemepad (Phase 3F).2//3// Diff vs hub v1:4// - Multiple admins (AddAdmin / RemoveAdmin / ListAdmins)5// - Any admin may SetModule / TransferAdmin rights via AddAdmin6// - Ops helpers: AdminCount, IsAdmin7//8// Migrate: deploy hubv2 → re-SetModule all paths → point UI HUB=hubv29package hubv21011import (12 "chain"13 "strconv"14 "strings"1516 "gno.land/p/nt/avl/v0"17)1819var (20 // admins: address string -> true21 admins avl.Tree22 inited bool23 // modules: name -> package path string24 modules avl.Tree25)2627// Init sets the first admin (deployer EOA). Call once.28func Init(cur realm) {29 if inited {30 panic("hubv2: already initialized")31 }32 if !cur.Previous().IsUserCall() {33 panic("hubv2: EOA only")34 }35 a := cur.Previous().Address()36 admins = avl.Tree{}37 modules = avl.Tree{}38 admins.Set(a.String(), true)39 inited = true40 chain.Emit("Init", "admin", a.String())41}4243func requireInit() {44 if !inited {45 panic("hubv2: call Init first")46 }47}4849func isAdminAddr(addr address) bool {50 return admins.Has(addr.String())51}5253func requireAdmin(cur realm) {54 requireInit()55 if !cur.Previous().IsUserCall() {56 panic("hubv2: EOA only")57 }58 if !isAdminAddr(cur.Previous().Address()) {59 panic("hubv2: not admin")60 }61}6263// IsAdmin reports whether addr is an admin.64func IsAdmin(addr string) bool {65 requireInit()66 return admins.Has(strings.TrimSpace(addr))67}6869// AdminCount returns number of admins.70func AdminCount() int {71 requireInit()72 return admins.Size()73}7475// ListAdmins returns newline-separated admin addresses.76func ListAdmins() string {77 requireInit()78 out := ""79 admins.Iterate("", "", func(key string, _ any) bool {80 if out != "" {81 out += "\n"82 }83 out += key84 return false85 })86 return out87}8889// AddAdmin grants admin rights (existing admin only).90func AddAdmin(cur realm, newAdmin address) {91 requireAdmin(cur)92 if !newAdmin.IsValid() {93 panic("hubv2: invalid address")94 }95 if isAdminAddr(newAdmin) {96 panic("hubv2: already admin")97 }98 admins.Set(newAdmin.String(), true)99 chain.Emit("AddAdmin", "addr", newAdmin.String())100}101102// RemoveAdmin revokes admin. Cannot remove last admin.103func RemoveAdmin(cur realm, who address) {104 requireAdmin(cur)105 if !who.IsValid() {106 panic("hubv2: invalid address")107 }108 if !isAdminAddr(who) {109 panic("hubv2: not an admin")110 }111 if admins.Size() <= 1 {112 panic("hubv2: cannot remove last admin")113 }114 // Prevent lock-out of caller if they're removing themselves as sole remaining — already size>1115 admins.Remove(who.String())116 chain.Emit("RemoveAdmin", "addr", who.String())117}118119// SetModule registers or updates a module path (any admin).120func SetModule(cur realm, name, path string) {121 requireAdmin(cur)122 name = strings.TrimSpace(name)123 path = strings.TrimSpace(path)124 if name == "" || len(name) > 32 {125 panic("hubv2: invalid name")126 }127 if path == "" || len(path) > 200 {128 panic("hubv2: invalid path")129 }130 if !strings.HasPrefix(path, "gno.land/") {131 panic("hubv2: path must start with gno.land/")132 }133 for _, c := range name {134 if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-') {135 panic("hubv2: name charset")136 }137 }138 modules.Set(name, path)139 chain.Emit("SetModule", "name", name, "path", path)140}141142// GetModule returns the package path for name, or "".143func GetModule(name string) string {144 requireInit()145 v := modules.Get(name)146 if v == nil {147 return ""148 }149 s, ok := v.(string)150 if !ok {151 return ""152 }153 return s154}155156// ListModules returns newline-separated "name|path".157func ListModules() string {158 requireInit()159 out := ""160 modules.Iterate("", "", func(key string, value any) bool {161 path, _ := value.(string)162 if out != "" {163 out += "\n"164 }165 out += key + "|" + path166 return false167 })168 return out169}170171// ModuleCount for ops.172func ModuleCount() int {173 requireInit()174 return modules.Size()175}176177// Inited reports Init.178func Inited() bool {179 return inited180}181182// Admin returns first admin (compat) — first key in tree iteration order.183func Admin() string {184 requireInit()185 out := ""186 admins.Iterate("", "", func(key string, _ any) bool {187 out = key188 return true189 })190 return out191}192193func Render(path string) string {194 path = strings.Trim(path, "/")195 if !inited {196 return "# gnomi hubv2\n\n> Not initialized.\n"197 }198 out := "# gnomi hubv2 (multi-admin)\n\n"199 out += "- Admins: **" + strconv.Itoa(admins.Size()) + "**\n"200 out += "- Modules: **" + strconv.Itoa(modules.Size()) + "**\n\n"201 out += "## Admins\n\n"202 admins.Iterate("", "", func(key string, _ any) bool {203 out += "- `" + key + "`\n"204 return false205 })206 out += "\n## Modules\n\n"207 modules.Iterate("", "", func(key string, value any) bool {208 p, _ := value.(string)209 out += "- **" + key + "**: `" + p + "`\n"210 return false211 })212 out += "\n## API\n\n- SetModule / GetModule / ListModules\n- AddAdmin / RemoveAdmin / ListAdmins / IsAdmin\n"213 _ = path214 return out215}216217func resetForTest() {218 admins = avl.Tree{}219 modules = avl.Tree{}220 inited = false221}222AddAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newAdmin string)
Admin() string
AdminCount() int
GetModule(name string) string
Init(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Inited() bool
IsAdmin(addr string) bool
ListAdmins() string
ListModules() string
ModuleCount() int
RemoveAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, who string)
Render(path string) string
SetModule(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, path string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.