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
  • store.gnogno
    1package users23import (4	"chain"5	"chain/runtime"6	"regexp"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/bptree/v0"
    9 "gno.land/p/nt/ufmt/v0"
    10)
    11
    12var (
    13 nameStore = bptree.NewBPTree32() // name/aliases > *UserData
    14 addressStore = bptree.NewBPTree32() // address > *UserData
    15
    16 reAddressLookalike = regexp.MustCompile(`^g1[a-z0-9]{20,38}$`)
    17
    18 // reName mirrors gno's package-name shape (gnovm/pkg/gnolang/mempackage.go
    19 // `Re_name`): start with a lowercase letter, optional alphanumeric body,
    20 // then any number of (separator + alphanumeric run) — so single hyphens
    21 // or underscores are allowed BETWEEN alphanumerics, but consecutive
    22 // separators (`--`, `__`, `-_`, `_-`) are rejected, and so are leading
    23 // or trailing separators. Lowercase-only — closes the case-confusable
    24 // squatting concern (Alice vs alice were two distinct names under the
    25 // previous case-preserving regex). Length cap of 64 enforced separately
    26 // in validateName.
    27 reName = regexp.MustCompile(`^[a-z][a-z0-9]*([_-][a-z0-9]+)*$`)
    28)
    29
    30const maxNameLen = 64
    31
    32const (
    33 RegisterUserEvent = "Registered"
    34 UpdateNameEvent = "Updated"
    35 DeleteUserEvent = "Deleted"
    36)
    37
    38type UserData struct {
    39 addr address
    40 username string // contains the latest name of a user
    41 deleted bool
    42}
    43
    44func (u UserData) Name() string {
    45 return u.username
    46}
    47
    48func (u UserData) Addr() address {
    49 return u.addr
    50}
    51
    52// IsDeleted reports whether this user record is missing or marked deleted.
    53// A nil receiver returns true — "the user does not exist" is semantically
    54// indistinguishable from "the user was deleted" for callers that need to
    55// gate further state changes. This lets call sites collapse the nil check
    56// and the deleted check into a single guard:
    57//
    58// if u.IsDeleted() {
    59// return ErrUserNotExistOrDeleted
    60// }
    61func (u *UserData) IsDeleted() bool {
    62 if u == nil {
    63 return true
    64 }
    65 return u.deleted
    66}
    67
    68// RenderLink provides a render link to the user page on gnoweb
    69// `linkText` is optional
    70func (u UserData) RenderLink(linkText string) string {
    71 if linkText == "" {
    72 return ufmt.Sprintf("[@%s](/u/%s)", u.username, u.username)
    73 }
    74
    75 return ufmt.Sprintf("[%s](/u/%s)", linkText, u.username)
    76}
    77
    78// registerUser adds a new user to the system without checking controllers.
    79// The ignoreCanonical flag suppresses ErrCanonicalCollision; the canonical
    80// store is written either way (decision #14: later-wins on bypass).
    81func registerUser(cur realm, name string, address_XXX address, ignoreCanonical bool) error {
    82 // Validate name
    83 if err := validateName(name); err != nil {
    84 return err
    85 }
    86
    87 // Validate address
    88 if !address_XXX.IsValid() {
    89 return ErrInvalidAddress
    90 }
    91
    92 // Check if name is taken (exact-string match precedes canonical check)
    93 if nameStore.Has(name) {
    94 return ErrNameTaken
    95 }
    96
    97 canonical := Canonicalize(name)
    98 if !ignoreCanonical {
    99 if canonicalStore.Has(canonical) {
    100 return ErrCanonicalCollision
    101 }
    102 }
    103
    104 raw := addressStore.Get(address_XXX.String())
    105 if raw != nil {
    106 // Cannot re-register after deletion
    107 if raw.(*UserData).IsDeleted() {
    108 return ErrDeletedUser
    109 }
    110
    111 // For a second name, use UpdateName
    112 return ErrAlreadyHasName
    113 }
    114
    115 // Create UserData
    116 data := &UserData{
    117 addr: address_XXX,
    118 username: name,
    119 deleted: false,
    120 }
    121
    122 // Set corresponding stores
    123 nameStore.Set(name, data)
    124 addressStore.Set(address_XXX.String(), data)
    125 canonicalStore.Set(canonical, name)
    126
    127 chain.Emit(RegisterUserEvent,
    128 "name", name,
    129 "address", address_XXX.String(),
    130 )
    131 return nil
    132}
    133
    134// RegisterUser adds a new user to the system. Enforces canonical-
    135// collision detection: a name whose Canonicalize-form matches a prior
    136// registration returns ErrCanonicalCollision.
    137func RegisterUser(cur realm, name string, address_XXX address) error {
    138 // IsCurrent before Previous, as UpdateName/Delete below already do. The
    139 // file was inconsistent: those three checked it, these two did not.
    140 if !cur.IsCurrent() {
    141 return NewErrNotWhitelisted(0, cur.Previous())
    142 }
    143 // At genesis (height 0), allow any caller to register users.
    144 // After genesis, only whitelisted controllers can register.
    145 if runtime.ChainHeight() > 0 && !controllers.Has(cur.Previous().Address()) {
    146 return NewErrNotWhitelisted(0, cur.Previous())
    147 }
    148
    149 return registerUser(cur, name, address_XXX, false)
    150}
    151
    152// RegisterUserIgnoreCanonical is the bypass path: same controller-
    153// whitelist gate, but ErrCanonicalCollision is suppressed. The canonical
    154// store is still written; a prior entry with the same canonical key is
    155// silently overwritten (decision #14, later-wins). Use sparingly — names
    156// registered here can canonical-collide with existing ones, weakening
    157// confusable protection for everyone.
    158func RegisterUserIgnoreCanonical(cur realm, name string, address_XXX address) error {
    159 // IsCurrent before Previous, as UpdateName/Delete below already do. The
    160 // file was inconsistent: those three checked it, these two did not.
    161 if !cur.IsCurrent() {
    162 return NewErrNotWhitelisted(0, cur.Previous())
    163 }
    164 if runtime.ChainHeight() > 0 && !controllers.Has(cur.Previous().Address()) {
    165 return NewErrNotWhitelisted(0, cur.Previous())
    166 }
    167
    168 return registerUser(cur, name, address_XXX, true)
    169}
    170
    171// updateName adds a name that is associated with a specific address without
    172// checking controllers. The ignoreCanonical flag suppresses
    173// ErrCanonicalCollision; the canonical store is written either way (decision
    174// #14: later-wins on bypass).
    175//
    176// All previous names are preserved and resolvable.
    177// The new name is the default value returned for address lookups.
    178func (u *UserData) updateName(newName string, ignoreCanonical bool) error {
    179 // IsDeleted handles both branches: nil receiver (user never existed)
    180 // AND a non-nil receiver whose .deleted is true (a controller cached
    181 // the *UserData pointer before the user was deleted by a separate
    182 // controller or governance proposal). Without the deleted-flag branch,
    183 // nameStore.Set(newName, u) would insert an alias pointing at a
    184 // deleted user — Has(newName) returns true forever but Resolve(newName)
    185 // returns nil (Resolve* APIs filter deleted), so the name is squatted
    186 // with no recovery path. (audit finding #3)
    187 if u.IsDeleted() {
    188 return ErrUserNotExistOrDeleted
    189 }
    190
    191 // Validate name
    192 if err := validateName(newName); err != nil {
    193 return err
    194 }
    195
    196 // Check if the requested Alias is already taken (exact-string match)
    197 if nameStore.Has(newName) {
    198 return ErrNameTaken
    199 }
    200
    201 canonical := Canonicalize(newName)
    202 if !ignoreCanonical {
    203 // No self-collision filter (decision #15): even the user's OWN
    204 // prior canonical claim blocks the rename. Prevents accumulating
    205 // confusable aliases of one's own name through free renames. The
    206 // only path to a self-confusable rename is DAO governance via
    207 // ProposeUpdateName.
    208 if canonicalStore.Has(canonical) {
    209 return ErrCanonicalCollision
    210 }
    211 }
    212
    213 u.username = newName
    214 nameStore.Set(newName, u)
    215 canonicalStore.Set(canonical, newName)
    216
    217 chain.Emit(UpdateNameEvent,
    218 "alias", newName,
    219 "address", u.addr.String(),
    220 )
    221 return nil
    222}
    223
    224// UpdateName adds a name that is associated with a specific address.
    225// Enforces canonical-collision detection.
    226// All previous names are preserved and resolvable.
    227// The new name is the default value returned for address lookups.
    228//
    229// rlm is the cur of the caller's enclosing crossing function (passed as
    230// data via the `_ int, rlm realm` non-crossing form). rlm.Address() is
    231// the calling realm against which we authorize.
    232func (u *UserData) UpdateName(_ int, rlm realm, newName string) error {
    233 if !rlm.IsCurrent() {
    234 return ErrInvalidRealm
    235 }
    236 if u.IsDeleted() {
    237 return ErrUserNotExistOrDeleted
    238 }
    239
    240 // Validate caller
    241 if !controllers.Has(rlm.Address()) {
    242 return NewErrNotWhitelisted(0, rlm)
    243 }
    244
    245 return u.updateName(newName, false)
    246}
    247
    248// UpdateNameIgnoreCanonical is the bypass path: same controller-
    249// whitelist gate, but ErrCanonicalCollision is suppressed. The canonical
    250// store is still written; a prior entry with the same canonical key is
    251// silently overwritten (decision #14, later-wins).
    252func (u *UserData) UpdateNameIgnoreCanonical(_ int, rlm realm, newName string) error {
    253 if !rlm.IsCurrent() {
    254 return ErrInvalidRealm
    255 }
    256 if u.IsDeleted() {
    257 return ErrUserNotExistOrDeleted
    258 }
    259
    260 if !controllers.Has(rlm.Address()) {
    261 return NewErrNotWhitelisted(0, rlm)
    262 }
    263
    264 return u.updateName(newName, true)
    265}
    266
    267// delete marks a user and all their aliases as deleted without checking controllers.
    268func (u *UserData) delete() error {
    269 if u.IsDeleted() {
    270 return ErrUserNotExistOrDeleted
    271 }
    272
    273 u.deleted = true
    274
    275 chain.Emit(DeleteUserEvent, "address", u.addr.String())
    276 return nil
    277}
    278
    279// Delete marks a user and all their aliases as deleted.
    280// rlm is the cur of the caller's enclosing crossing function; see UpdateName.
    281func (u *UserData) Delete(_ int, rlm realm) error {
    282 if !rlm.IsCurrent() {
    283 return ErrInvalidRealm
    284 }
    285 if u.IsDeleted() {
    286 return ErrUserNotExistOrDeleted
    287 }
    288
    289 // Validate caller
    290 if !controllers.Has(rlm.Address()) {
    291 return NewErrNotWhitelisted(0, rlm)
    292 }
    293
    294 return u.delete()
    295}
    296
    297// Validate validates username and address passed in
    298// Most of the validation is done in the controllers
    299// This provides more flexibility down the line
    300func validateName(username string) error {
    301 if username == "" {
    302 return ErrEmptyUsername
    303 }
    304
    305 if len(username) > maxNameLen {
    306 return ErrInvalidUsername
    307 }
    308
    309 if !reName.MatchString(username) {
    310 return ErrInvalidUsername
    311 }
    312
    313 // Check if the username can be decoded or looks like a valid address
    314 if address(username).IsValid() || reAddressLookalike.MatchString(username) {
    315 return ErrNameLikeAddress
    316 }
    317
    318 return nil
    319}
    320