hub.gno
4.88 Kb · 221 lines
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 AddAdmin
6// - Ops helpers: AdminCount, IsAdmin
7//
8// Migrate: deploy hubv2 → re-SetModule all paths → point UI HUB=hubv2
9package hubv2
10
11import (
12 "chain"
13 "strconv"
14 "strings"
15
16 "gno.land/p/nt/avl/v0"
17)
18
19var (
20 // admins: address string -> true
21 admins avl.Tree
22 inited bool
23 // modules: name -> package path string
24 modules avl.Tree
25)
26
27// 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 = true
40 chain.Emit("Init", "admin", a.String())
41}
42
43func requireInit() {
44 if !inited {
45 panic("hubv2: call Init first")
46 }
47}
48
49func isAdminAddr(addr address) bool {
50 return admins.Has(addr.String())
51}
52
53func 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}
62
63// IsAdmin reports whether addr is an admin.
64func IsAdmin(addr string) bool {
65 requireInit()
66 return admins.Has(strings.TrimSpace(addr))
67}
68
69// AdminCount returns number of admins.
70func AdminCount() int {
71 requireInit()
72 return admins.Size()
73}
74
75// 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 += key
84 return false
85 })
86 return out
87}
88
89// 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}
101
102// 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>1
115 admins.Remove(who.String())
116 chain.Emit("RemoveAdmin", "addr", who.String())
117}
118
119// 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}
141
142// 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 s
154}
155
156// 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 + "|" + path
166 return false
167 })
168 return out
169}
170
171// ModuleCount for ops.
172func ModuleCount() int {
173 requireInit()
174 return modules.Size()
175}
176
177// Inited reports Init.
178func Inited() bool {
179 return inited
180}
181
182// 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 = key
188 return true
189 })
190 return out
191}
192
193func Render(path string) string {
194 path = strings.Trim(path, "/")
195 if !inited {
196 return "# gnomemepad hubv2\n\n> Not initialized.\n"
197 }
198 out := "# gnomemepad 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 false
205 })
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 false
211 })
212 out += "\n## API\n\n- SetModule / GetModule / ListModules\n- AddAdmin / RemoveAdmin / ListAdmins / IsAdmin\n"
213 _ = path
214 return out
215}
216
217func resetForTest() {
218 admins = avl.Tree{}
219 modules = avl.Tree{}
220 inited = false
221}