hub.gno
3.70 Kb · 166 lines
1// Package hub is the version/module router for gnomemepad.
2//
3// UI and tooling should resolve the active pad (and other modules) via hub
4// instead of hardcoding padvN paths. Admins rotate module paths without
5// redeploying every consumer.
6//
7// Init → SetModule("pad", "gno.land/r/.../padv4") → GetModule("pad")
8package hub
9
10import (
11 "chain"
12 "strings"
13
14 "gno.land/p/nt/avl/v0"
15)
16
17var (
18 admin address
19 inited bool
20 // modules: name -> package path string
21 modules avl.Tree
22)
23
24// Init sets the hub admin (first EOA). Call once after deploy.
25func Init(cur realm) {
26 if inited {
27 panic("hub: already initialized")
28 }
29 if !cur.Previous().IsUserCall() {
30 panic("hub: EOA only")
31 }
32 admin = cur.Previous().Address()
33 inited = true
34 modules = avl.Tree{}
35 chain.Emit("Init", "admin", admin.String())
36}
37
38func requireInit() {
39 if !inited {
40 panic("hub: call Init first")
41 }
42}
43
44func requireAdmin(cur realm) {
45 requireInit()
46 if !cur.Previous().IsUserCall() {
47 panic("hub: EOA only")
48 }
49 if cur.Previous().Address() != admin {
50 panic("hub: not admin")
51 }
52}
53
54// SetModule registers or updates a module path (admin only).
55// name: short key e.g. "pad", "profile", "legacy_pad"
56// path: full gno package path
57func SetModule(cur realm, name, path string) {
58 requireAdmin(cur)
59 name = strings.TrimSpace(name)
60 path = strings.TrimSpace(path)
61 if name == "" || len(name) > 32 {
62 panic("hub: invalid name")
63 }
64 if path == "" || len(path) > 200 {
65 panic("hub: invalid path")
66 }
67 if !strings.HasPrefix(path, "gno.land/") {
68 panic("hub: path must start with gno.land/")
69 }
70 for _, c := range name {
71 if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-') {
72 panic("hub: name charset")
73 }
74 }
75 modules.Set(name, path)
76 chain.Emit("SetModule", "name", name, "path", path)
77}
78
79// GetModule returns the package path for name, or "" if unset.
80func GetModule(name string) string {
81 requireInit()
82 v := modules.Get(name)
83 if v == nil {
84 return ""
85 }
86 s, ok := v.(string)
87 if !ok {
88 return ""
89 }
90 return s
91}
92
93// ListModules returns newline-separated "name|path" lines (sorted by name).
94func ListModules() string {
95 requireInit()
96 out := ""
97 modules.Iterate("", "", func(key string, value any) bool {
98 path, _ := value.(string)
99 if out != "" {
100 out += "\n"
101 }
102 out += key + "|" + path
103 return false
104 })
105 return out
106}
107
108// TransferAdmin rotates hub admin.
109func TransferAdmin(cur realm, newAdmin address) {
110 requireAdmin(cur)
111 if !newAdmin.IsValid() {
112 panic("hub: invalid address")
113 }
114 if newAdmin == admin {
115 panic("hub: same admin")
116 }
117 old := admin
118 admin = newAdmin
119 chain.Emit("TransferAdmin", "from", old.String(), "to", newAdmin.String())
120}
121
122// Admin returns the hub admin address.
123func Admin() string {
124 requireInit()
125 return admin.String()
126}
127
128// Inited reports whether Init was called.
129func Inited() bool {
130 return inited
131}
132
133// Render — discovery home.
134func Render(path string) string {
135 path = strings.Trim(path, "/")
136 if !inited {
137 return "# gnomemepad hub\n\n> Not initialized. Call Init.\n"
138 }
139 out := "# gnomemepad hub\n\n"
140 out += "- Admin: `" + admin.String() + "`\n\n"
141 out += "## Modules\n\n"
142 n := 0
143 modules.Iterate("", "", func(key string, value any) bool {
144 path, _ := value.(string)
145 out += "- **" + key + "**: `" + path + "`\n"
146 n++
147 return false
148 })
149 if n == 0 {
150 out += "_No modules registered. Admin: SetModule(name, path)._\n"
151 }
152 out += "\n## API\n\n"
153 out += "- `GetModule(name)` / `ListModules()`\n"
154 out += "- `SetModule(name, path)` — admin\n"
155 out += "- `TransferAdmin(newAdmin)` — admin\n"
156 _ = path
157 return out
158}
159
160// resetForTest — unit tests only (unexported).
161func resetForTest() {
162 var zero address
163 admin = zero
164 inited = false
165 modules = avl.Tree{}
166}