PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/sys/users

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
users
Namespace
sys
Files
12 (README)(gnomod.toml)
Exported functions
21
Module
gno.land/r/sys/users
gno
0.9

Files (12)

  • README.mdmarkdown
  • gnomod.tomltoml
  • admin.gnogno
  • api.gnogno
  • canonical.gnogno
  • errors.gnogno
  • render.gnogno
  • store.gnogno
  • users.gnogno
  • crossrealm_test.gnogno
  • store_test.gnogno
  • users_test.gnogno
  • users_test.gnogno
    1package users23import (4	"strconv"5	"strings"6	"testing"78

    Functions

    • 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.

    Rendered

    RenderedRawgnoweb ↗

    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/testutils/v0"
    9 "gno.land/p/nt/uassert/v0"
    10 "gno.land/p/nt/urequire/v0"
    11)
    12
    13// cur is a zero-value realm used as a placeholder when forwarding to
    14// uassert/urequire dispatch helpers that gained an `rlm realm` param.
    15// These tests pass `func()` callbacks (no crossing inside the callback),
    16// so rlm is ignored — a nil realm here is safe.
    17var cur realm
    18
    19func TestResolveName(cur realm, t *testing.T) {
    20 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
    21
    22 t.Run("single_name", func(t *testing.T) {
    23 cleanStore(t)
    24
    25 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    26
    27 res, isLatest := ResolveName(alice)
    28 uassert.Equal(t, aliceAddr, res.Addr())
    29 uassert.Equal(t, alice, res.Name())
    30 uassert.True(t, isLatest)
    31 })
    32
    33 t.Run("name+Alias", func(t *testing.T) {
    34 cleanStore(t)
    35
    36 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    37 data, _ := ResolveName(alice)
    38 urequire.NoError(t, data.UpdateName(0, cur, "alice1"))
    39
    40 res, isLatest := ResolveName("alice1")
    41 urequire.NotEqual(t, nil, res)
    42
    43 uassert.Equal(t, aliceAddr, res.Addr())
    44 uassert.Equal(t, "alice1", res.Name())
    45 uassert.True(t, isLatest)
    46 })
    47
    48 t.Run("multiple_aliases", func(t *testing.T) {
    49 cleanStore(t)
    50
    51 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    52
    53 // RegisterUser and check each Alias
    54 var names []string
    55 names = append(names, alice)
    56 for i := 0; i < 5; i++ {
    57 alias := "alice" + strconv.Itoa(i)
    58 names = append(names, alias)
    59
    60 data, _ := ResolveName(alice)
    61 urequire.NoError(t, data.UpdateName(0, cur, alias))
    62 }
    63
    64 for _, alias := range names {
    65 res, _ := ResolveName(alias)
    66 urequire.NotEqual(t, nil, res)
    67
    68 uassert.Equal(t, aliceAddr, res.Addr())
    69 uassert.Equal(t, "alice4", res.Name())
    70 }
    71 })
    72}
    73
    74func TestResolveAddress(cur realm, t *testing.T) {
    75 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
    76
    77 t.Run("single_name", func(t *testing.T) {
    78 cleanStore(t)
    79
    80 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    81
    82 res := ResolveAddress(aliceAddr)
    83
    84 uassert.Equal(t, aliceAddr, res.Addr())
    85 uassert.Equal(t, alice, res.Name())
    86 })
    87
    88 t.Run("name+Alias", func(t *testing.T) {
    89 cleanStore(t)
    90
    91 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    92 data, _ := ResolveName(alice)
    93 urequire.NoError(t, data.UpdateName(0, cur, "alice1"))
    94
    95 res := ResolveAddress(aliceAddr)
    96 urequire.NotEqual(t, nil, res)
    97
    98 uassert.Equal(t, aliceAddr, res.Addr())
    99 uassert.Equal(t, "alice1", res.Name())
    100 })
    101
    102 t.Run("multiple_aliases", func(t *testing.T) {
    103 cleanStore(t)
    104
    105 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    106
    107 // RegisterUser and check each Alias
    108 var names []string
    109 names = append(names, alice)
    110
    111 for i := 0; i < 5; i++ {
    112 alias := "alice" + strconv.Itoa(i)
    113 names = append(names, alias)
    114 data, _ := ResolveName(alice)
    115 urequire.NoError(t, data.UpdateName(0, cur, alias))
    116 }
    117
    118 res := ResolveAddress(aliceAddr)
    119 uassert.Equal(t, aliceAddr, res.Addr())
    120 uassert.Equal(t, "alice4", res.Name())
    121 })
    122}
    123
    124func TestROStores(cur realm, t *testing.T) {
    125 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
    126 cleanStore(t)
    127
    128 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    129 roNS := GetReadOnlyNameStore()
    130 roAS := GetReadonlyAddrStore()
    131
    132 t.Run("get user data", func(t *testing.T) {
    133 // Name store
    134 aliceDataRaw := roNS.Get(alice)
    135 urequire.NotNil(t, aliceDataRaw)
    136
    137 roData, ok := aliceDataRaw.(*UserData)
    138 uassert.True(t, ok, "Could not cast data from RO tree to UserData")
    139
    140 // Try to modify data
    141 roData.Delete(0, cur)
    142 raw := nameStore.Get(alice)
    143 uassert.False(t, raw.(*UserData).deleted)
    144
    145 // Addr store
    146 aliceDataRaw = roAS.Get(aliceAddr.String())
    147 urequire.NotNil(t, aliceDataRaw)
    148
    149 roData, ok = aliceDataRaw.(*UserData)
    150 uassert.True(t, ok, "Could not cast data from RO tree to UserData")
    151
    152 // Try to modify data
    153 roData.Delete(0, cur)
    154 raw = nameStore.Get(alice)
    155 uassert.False(t, raw.(*UserData).deleted)
    156 })
    157
    158 t.Run("get deleted data", func(t *testing.T) {
    159 raw := nameStore.Get(alice)
    160 aliceData := raw.(*UserData)
    161
    162 urequire.NoError(t, aliceData.Delete(0, cur))
    163 urequire.True(t, aliceData.IsDeleted())
    164
    165 // Should be nil because of makeSafeFn intercepting the value.
    166 rawRoData := roNS.Get(alice)
    167 uassert.Equal(t, rawRoData, nil)
    168 _, ok := rawRoData.(*UserData) // shouldn't be castable
    169 uassert.False(t, ok)
    170 })
    171}
    172
    173func TestResolveAny(cur realm, t *testing.T) {
    174 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
    175
    176 t.Run("name", func(t *testing.T) {
    177 cleanStore(t)
    178
    179 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    180
    181 res, _ := ResolveAny(alice)
    182
    183 uassert.Equal(t, aliceAddr, res.Addr())
    184 uassert.Equal(t, alice, res.Name())
    185 })
    186
    187 t.Run("address", func(t *testing.T) {
    188 cleanStore(t)
    189
    190 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
    191
    192 res, _ := ResolveAny(aliceAddr.String())
    193
    194 uassert.Equal(t, aliceAddr, res.Addr())
    195 uassert.Equal(t, alice, res.Name())
    196 })
    197
    198 t.Run("not_registered", func(t *testing.T) {
    199 cleanStore(t)
    200
    201 res, _ := ResolveAny(aliceAddr.String())
    202
    203 uassert.Equal(t, nil, res)
    204 })
    205}
    206
    207func TestProposeErrors(cur realm, t *testing.T) {
    208 t.Run("propose_register_user_errors", func(t *testing.T) {
    209 urequire.PanicsWithMessage(t, cur, ErrInvalidUsername.Error(), func() {
    210 ProposeRegisterUser(cur, "bad name", aliceAddr)
    211 })
    212 urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {
    213 ProposeRegisterUser(cur, alice, "badaddress")
    214 })
    215 })
    216
    217 t.Run("propose_update_name_errors", func(t *testing.T) {
    218 cleanStore(t)
    219
    220 urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {
    221 ProposeUpdateName(cur, "badaddress", "alice1")
    222 })
    223 urequire.PanicsWithMessage(t, cur, ErrInvalidUsername.Error(), func() {
    224 ProposeUpdateName(cur, aliceAddr, "bad name")
    225 })
    226 // Note: unregistered user is not checked at proposal creation time.
    227 // The callback handles it at execution time.
    228 })
    229
    230 t.Run("propose_delete_user_errors", func(t *testing.T) {
    231 cleanStore(t)
    232
    233 urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {
    234 ProposeDeleteUser(cur, "badaddress")
    235 })
    236 // Note: unregistered user is not checked at proposal creation time.
    237 // The callback handles it at execution time.
    238 })
    239}
    240
    241// Audit finding #6: ProposeControllerAdditionAndRemoval used to return early
    242// from its callback when toAdd was already whitelisted (addToWhitelist
    243// returned ErrAlreadyWhitelisted), skipping the remove step entirely. A
    244// passed swap proposal would silently leave the old controller active.
    245//
    246// applyControllerSwap is the extracted callback body. These tests exercise
    247// it directly to confirm:
    248// - normal swap (toAdd new, toRemove present) succeeds and updates state
    249// - toAdd already present is benign — remove still happens
    250// - toRemove already absent is benign — add still happens, no error
    251// - errors that aren't the idempotency cases still propagate
    252func TestApplyControllerSwap(t *testing.T) {
    253 // Use unique addresses per subtest to avoid having to drain the
    254 // controllers set (addrset.Set has no clear/iterate-all method, and
    255 // state persists across subtests in this package's tests).
    256 t.Run("normal swap A->B", func(t *testing.T) {
    257 a := testutils.TestAddress("swapA1")
    258 b := testutils.TestAddress("swapB1")
    259 controllers.Add(a)
    260 defer controllers.Remove(b)
    261
    262 uassert.NoError(t, applyControllerSwap(b, a))
    263 uassert.True(t, controllers.Has(b), "b should be whitelisted")
    264 uassert.False(t, controllers.Has(a), "a should be removed")
    265 })
    266
    267 t.Run("toAdd already whitelisted, toRemove present", func(t *testing.T) {
    268 // Regression for audit finding #6. Before the fix, this returned
    269 // ErrAlreadyWhitelisted from the swap callback, skipping the remove
    270 // step entirely — the swap silently no-op'd and the old controller
    271 // stayed active.
    272 a := testutils.TestAddress("swapA2")
    273 b := testutils.TestAddress("swapB2")
    274 controllers.Add(a)
    275 controllers.Add(b)
    276 defer controllers.Remove(b)
    277
    278 uassert.NoError(t, applyControllerSwap(b, a))
    279 uassert.True(t, controllers.Has(b), "b should remain whitelisted")
    280 uassert.False(t, controllers.Has(a), "a should be removed even though b was already in")
    281 })
    282
    283 t.Run("toRemove already absent, toAdd new", func(t *testing.T) {
    284 a := testutils.TestAddress("swapA3")
    285 b := testutils.TestAddress("swapB3")
    286 c := testutils.TestAddress("swapC3") // never added
    287 controllers.Add(a)
    288 defer controllers.Remove(a)
    289 defer controllers.Remove(b)
    290
    291 // Remove c which isn't in the set; should be benign.
    292 uassert.NoError(t, applyControllerSwap(b, c))
    293 uassert.True(t, controllers.Has(b), "b should be whitelisted")
    294 uassert.True(t, controllers.Has(a), "a should still be whitelisted (unchanged)")
    295 uassert.False(t, controllers.Has(c), "c was never in the set")
    296 })
    297
    298 t.Run("both idempotency cases at once", func(t *testing.T) {
    299 // toAdd already in, toRemove already out — should succeed cleanly,
    300 // no state change.
    301 a := testutils.TestAddress("swapA4")
    302 b := testutils.TestAddress("swapB4")
    303 c := testutils.TestAddress("swapC4")
    304 controllers.Add(a)
    305 controllers.Add(b)
    306 defer controllers.Remove(a)
    307 defer controllers.Remove(b)
    308
    309 uassert.NoError(t, applyControllerSwap(b, c))
    310 uassert.True(t, controllers.Has(a))
    311 uassert.True(t, controllers.Has(b))
    312 uassert.False(t, controllers.Has(c))
    313 })
    314}
    315
    316// Audit finding #20: the controller whitelist must be queryable from
    317// outside the package so operators can monitor authority without
    318// source-diving or replaying governance proposals.
    319func TestControllerQueries(t *testing.T) {
    320 t.Run("IsController reflects whitelist state", func(t *testing.T) {
    321 a := testutils.TestAddress("queryA1")
    322 b := testutils.TestAddress("queryB1")
    323
    324 // Before any add: both report false.
    325 uassert.False(t, IsController(a))
    326 uassert.False(t, IsController(b))
    327
    328 controllers.Add(a)
    329 defer controllers.Remove(a)
    330
    331 uassert.True(t, IsController(a))
    332 uassert.False(t, IsController(b))
    333 })
    334
    335 t.Run("Controllers returns a snapshot of current whitelist", func(t *testing.T) {
    336 // Use distinct addresses that aren't already in the set from
    337 // other tests in this file.
    338 a := testutils.TestAddress("queryA2")
    339 b := testutils.TestAddress("queryB2")
    340 c := testutils.TestAddress("queryC2")
    341
    342 controllers.Add(a)
    343 controllers.Add(b)
    344 controllers.Add(c)
    345 defer controllers.Remove(a)
    346 defer controllers.Remove(b)
    347 defer controllers.Remove(c)
    348
    349 got := Controllers()
    350
    351 // All three must appear (regardless of order vs other tests'
    352 // leftover entries — we only assert ours are present).
    353 seen := map[string]bool{}
    354 for _, e := range got {
    355 seen[e.String()] = true
    356 }
    357 uassert.True(t, seen[a.String()], "snapshot must contain a")
    358 uassert.True(t, seen[b.String()], "snapshot must contain b")
    359 uassert.True(t, seen[c.String()], "snapshot must contain c")
    360 })
    361
    362 t.Run("Controllers returns a copy — caller mutation does not affect realm", func(t *testing.T) {
    363 a := testutils.TestAddress("queryA3")
    364 controllers.Add(a)
    365 defer controllers.Remove(a)
    366
    367 got := Controllers()
    368 // Mutate the returned slice. The realm's controllers set must not
    369 // be affected.
    370 got = got[:0]
    371
    372 uassert.True(t, controllers.Has(a), "realm state must be unaffected by caller-side slice mutation")
    373 uassert.True(t, IsController(a))
    374 })
    375}
    376
    377// ProposeRegisterUser auto-injects a CANONICAL COLLISION warning into
    378// the proposal description when the proposed name canonical-collides
    379// with an existing registration. The warning surfaces the colliding
    380// existing name to voters; the proposal still goes through if voted in
    381// (decision #3, DAO grants always bypass).
    382func TestProposeRegisterUser_CollisionWarning(cur realm, t *testing.T) {
    383 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
    384
    385 t.Run("warning_injected_on_collision", func(t *testing.T) {
    386 cleanStore(t)
    387 urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
    388
    389 req := ProposeRegisterUser(cur, "vital1k", bobAddr)
    390 uassert.True(t,
    391 strings.Contains(req.Description(), "CANONICAL COLLISION"),
    392 "description must include collision warning")
    393 uassert.True(t,
    394 strings.Contains(req.Description(), "`vitalik`"),
    395 "description must name the existing colliding registration")
    396 })
    397
    398 t.Run("no_warning_when_no_collision", func(t *testing.T) {
    399 cleanStore(t)
    400
    401 req := ProposeRegisterUser(cur, "freshname", aliceAddr)
    402 uassert.False(t,
    403 strings.Contains(req.Description(), "CANONICAL COLLISION"),
    404 "description must not include collision warning when name is fresh")
    405 })
    406}
    407
    408// Note: full execution of ProposeRegisterUser/ProposeUpdateName closures
    409// (with ignoreCanonical=true → later-wins overwrite) is exercised end-to-
    410// end by the integration test in gno.land/pkg/integration/testdata/.
    411// The bypass-write semantics themselves are covered here at the unit
    412// level by TestRegisterUserIgnoreCanonical and TestUpdateName_CanonicalCollision.
    413
    414// TODO Uncomment after gnoweb /u/ page.
    415//func TestUserRenderLink(cur realm, t *testing.T) {
    416// testing.SetOriginCaller(whitelistedCallerAddr)
    417// cleanStore(t)
    418//
    419// urequire.NoError(t, RegisterUser(alice, aliceAddr))
    420//
    421// data, _ := ResolveName(alice)
    422// uassert.Equal(t, data.RenderLink(""), ufmt.Sprintf("[@%s](/u/%s)", alice, alice))
    423// text := "my link text!"
    424// uassert.Equal(t, data.RenderLink(text), ufmt.Sprintf("[%s](/u/%s)", text, alice))
    425//}
    426