Kind Realm (renderable)
Name users
Namespace sys
Exported functions 21
Module gno.land/r/sys/users
gno 0.9 admin.gno gno
⧉
1 package users 2 3 import ( 4 "chain" 5 "chain/runtime" 6 7 "gno.land/p/moul/addrset/v0" 8 AddControllerAtGenesis (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}, addr string)
Canonicalize (name string) string
Controllers () [].uverse.address
GetReadonlyAddrStore () *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree
GetReadOnlyNameStore () *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree
IsCanonicalTaken (name string) (existing string, taken bool)
IsController (addr string) bool
IsNameTaken (name string) bool
NewErrNotWhitelisted (int, caller 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}) struct{Caller string}
ProposeControllerAdditionAndRemoval (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}, toAdd string, toRemove string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeControllerRemoval (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}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeDeleteUser (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}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeNewController (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}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeRegisterUser (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, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeUpdateName (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}, addr string, newName string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
RegisterUser (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, address_XXX string) interface {Error func() string}
RegisterUserIgnoreCanonical (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, address_XXX string) interface {Error func() string}
Render (string) string
ResolveAddress (addr string) *gno.land/r/sys/users.UserData
ResolveAny (input string) (*gno.land/r/sys/users.UserData, bool)
ResolveName (name string) (data *gno.land/r/sys/users.UserData, isCurrent bool)
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.
"gno.land/p/nt/ufmt/v0"
9
10 "gno.land/r/gov/dao"
11 )
12
13 const initControllerPath = "gno.land/r/sys/users/init"
14
15 var controllers = addrset.Set{} // caller whitelist
16
17 func init() {
18 // auto-whitelist the init controller for bootstrapping for testing chain.
19 if chainID := runtime.ChainID(); chainID == "dev" {
20 controllers.Add(chain.PackageAddress(initControllerPath))
21 }
22 }
23
24 // AddControllerAtGenesis allows adding a controller during chain genesis (height 0).
25 // This is mostly useful for testing.
26 func AddControllerAtGenesis(_ realm, addr address) {
27 height := runtime.ChainHeight()
28 if height > 0 {
29 panic( "AddControllerAtGenesis can only be called at genesis (height 0)" )
30 }
31
32 if !addr.IsValid() {
33 panic(ErrInvalidAddress)
34 }
35
36 controllers.Add(addr)
37 }
38
39 // ProposeNewController allows GovDAO to add a whitelisted caller
40 func ProposeNewController(cur realm, addr address) dao.ProposalRequest {
41 if !addr.IsValid() {
42 panic(ErrInvalidAddress)
43 }
44
45 cb := func (cur realm) error {
46 return addToWhitelist(addr)
47 }
48
49 desc := "This proposal adds " + addr.String() + " to `sys/users` realm's callers whitelist."
50 return dao.NewProposalRequest( "Add Whitelisted Caller to \"sys/users\" Realm" , desc, dao.NewSimpleExecutor( 0 , cur, cb, "" ))
51 }
52
53 // ProposeControllerRemoval allows GovDAO to add a whitelisted caller
54 func ProposeControllerRemoval(cur realm, addr address) dao.ProposalRequest {
55 if !addr.IsValid() {
56 panic(ErrInvalidAddress)
57 }
58
59 cb := func (cur realm) error {
60 return deleteFromWhitelist(addr)
61 }
62
63 desc := "This proposal removes " + addr.String() + " from `sys/users` realm's callers whitelist."
64 return dao.NewProposalRequest( "Remove Whitelisted Caller From \"sys/users\" Realm" , desc, dao.NewSimpleExecutor( 0 , cur, cb, "" ))
65 }
66
67 // ProposeControllerAdditionAndRemoval allows GovDAO to add a new caller and remove an old caller in the same proposal.
68 func ProposeControllerAdditionAndRemoval(cur realm, toAdd, toRemove address) dao.ProposalRequest {
69 if !toAdd.IsValid() || !toRemove.IsValid() {
70 panic(ErrInvalidAddress)
71 }
72
73 cb := func (cur realm) error {
74 return applyControllerSwap(toAdd, toRemove)
75 }
76
77 desc := ufmt.Sprintf(
78 "This proposal adds %s and removes %s from `sys/users` realm's callers whitelist." ,
79 toAdd,
80 toRemove,
81 )
82 return dao.NewProposalRequest( "Add and Remove Whitelisted Callers From \"sys/users\" Realm" , desc, dao.NewSimpleExecutor( 0 , cur, cb, "" ))
83 }
84
85 // applyControllerSwap is the callback body of ProposeControllerAdditionAndRemoval,
86 // extracted so it can be unit-tested without driving the full GovDAO flow.
87 //
88 // The desired end state is "toAdd is in the whitelist AND toRemove is out".
89 // Both operations are made idempotent so the swap doesn't silently no-op when
90 // the chain state has drifted between proposal creation and execution:
91 //
92 // - If toAdd is already whitelisted, treat addToWhitelist's
93 // ErrAlreadyWhitelisted as benign and continue to the remove step.
94 // - If toRemove is already absent, treat deleteFromWhitelist's
95 // ErrNotWhitelisted as benign and return success.
96 //
97 // Without this idempotency, the original code returned early on an "already
98 // whitelisted" toAdd and skipped the remove entirely — a swap proposal could
99 // pass governance and silently leave the old controller active. (audit
100 // finding #6)
101 func applyControllerSwap(toAdd, toRemove address) error {
102 if err := addToWhitelist(toAdd); err != nil && err != ErrAlreadyWhitelisted {
103 return err
104 }
105 if err := deleteFromWhitelist(toRemove); err != nil {
106 if _, alreadyOut := err.(ErrNotWhitelisted); !alreadyOut {
107 return err
108 }
109 }
110 return nil
111 }
112
113 // ProposeRegisterUser allows GovDAO to register a name without checking
114 // controllers. The executor closure runs with ignoreCanonical=true (decision
115 // #3): DAO grants always bypass canonical-collision detection. Voters see
116 // any collision in the proposal description and can vote NO if unintended.
117 func ProposeRegisterUser(cur realm, name string , addr address) dao.ProposalRequest {
118 // Validate the name and address now, even though registerUser will validate again
119 if err := validateName(name); err != nil {
120 panic(err.Error())
121 }
122 if !addr.IsValid() {
123 panic(ErrInvalidAddress)
124 }
125
126 desc := "This proposal registers " + name + " with address " + addr.String() + " in `sys/users`."
127 if existing, taken := IsCanonicalTaken(name); taken && existing != name {
128 desc += "\n\nCANONICAL COLLISION: this name's canonical form matches the existing registration of `" +
129 existing + "`. DAO grants bypass canonical-collision detection — the proposal will succeed if voted in. " +
130 "If the collision is unintended, vote NO."
131 }
132
133 cb := func (cur realm) error {
134 return registerUser(cur, name, addr, true ) // bypass canonical (decision #3)
135 }
136
137 return dao.NewProposalRequest( "Register User to \"sys/users\" Realm" , desc, dao.NewSimpleExecutor( 0 , cur, cb, "" ))
138 }
139
140 // ProposeUpdateName allows GovDAO to update a name with an alias without
141 // checking controllers. Like ProposeRegisterUser, the executor runs with
142 // ignoreCanonical=true (decision #3).
143 func ProposeUpdateName(cur realm, addr address, newName string ) dao.ProposalRequest {
144 if !addr.IsValid() {
145 panic(ErrInvalidAddress)
146 }
147 if err := validateName(newName); err != nil {
148 panic(err.Error())
149 }
150
151 desc := "This proposal updates address " + addr.String() + " with alias " + newName + " in `sys/users`."
152 if existing, taken := IsCanonicalTaken(newName); taken && existing != newName {
153 desc += "\n\nCANONICAL COLLISION: the new alias's canonical form matches the existing registration of `" +
154 existing + "`. DAO grants bypass canonical-collision detection — the proposal will succeed if voted in. " +
155 "If the collision is unintended, vote NO."
156 }
157
158 cb := func (cur realm) error {
159 data := ResolveAddress(addr)
160 if data == nil {
161 return ErrUserNotExistOrDeleted
162 }
163 return data.updateName(newName, true ) // bypass canonical (decision #3)
164 }
165
166 return dao.NewProposalRequest( "Update Name Alias in \"sys/users\" Realm" , desc, dao.NewSimpleExecutor( 0 , cur, cb, "" ))
167 }
168
169 // ProposeDeleteUser allows GovDAO to delete a user without checking controllers
170 func ProposeDeleteUser(cur realm, addr address) dao.ProposalRequest {
171 if !addr.IsValid() {
172 panic(ErrInvalidAddress)
173 }
174
175 cb := func (cur realm) error {
176 data := ResolveAddress(addr)
177 if data == nil {
178 return ErrUserNotExistOrDeleted
179 }
180 return data.delete()
181 }
182
183 desc := "This proposal deletes the user with address " + addr.String() + " in `sys/users`."
184 return dao.NewProposalRequest( "Delete User in \"sys/users\" Realm" , desc, dao.NewSimpleExecutor( 0 , cur, cb, "" ))
185 }
186
187 // IsController reports whether the given address is currently in the
188 // controller whitelist. Returns the same boolean that gating checks
189 // (RegisterUser, UpdateName, Delete) use internally — useful for
190 // off-chain monitoring and for governance proposals to inspect state
191 // before voting.
192 func IsController(addr address) bool {
193 return controllers.Has(addr)
194 }
195
196 // Controllers returns a snapshot of the current controller whitelist.
197 // The returned slice is a fresh copy; mutating it does not affect realm
198 // state. Order is the iteration order of the underlying address set.
199 //
200 // Audit finding #20: without this getter, the controller whitelist was
201 // opaque from outside the package — operators had to read source or
202 // replay every governance proposal to know who could write to the
203 // registry. This is the read-only API that closes that gap.
204 func Controllers() []address {
205 out := make([]address, 0 , controllers.Size())
206 controllers.IterateByOffset( 0 , controllers.Size(), func (a address) bool {
207 out = append(out, a)
208 return false
209 })
210 return out
211 }
212
213 // Helpers
214
215 func deleteFromWhitelist(addr address) error {
216 if !controllers.Has(addr) {
217 return ErrNotWhitelisted{Caller: "UserRealm{ " + addr.String() + " }" }
218 }
219
220 if ok := controllers.Remove(addr); !ok {
221 return ErrWhitelistRemoveFailed
222 }
223
224 return nil
225 }
226
227 func addToWhitelist(newCaller address) error {
228 if !controllers.Add(newCaller) {
229 return ErrAlreadyWhitelisted
230 }
231
232 return nil
233 }
234