github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/examples/gno.land/r/demo/users/users.gno (about) 1 package users 2 3 import ( 4 "regexp" 5 "std" 6 "strconv" 7 "strings" 8 9 "gno.land/p/demo/avl" 10 "gno.land/p/demo/users" 11 ) 12 13 //---------------------------------------- 14 // State 15 16 var ( 17 admin std.Address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj" 18 name2User avl.Tree // Name -> *users.User 19 addr2User avl.Tree // std.Address -> *users.User 20 invites avl.Tree // string(inviter+":"+invited) -> true 21 counter int // user id counter 22 minFee int64 = 200 * 1000000 // minimum gnot must be paid to register. 23 maxFeeMult int64 = 10 // maximum multiples of minFee accepted. 24 ) 25 26 //---------------------------------------- 27 // Top-level functions 28 29 func Register(inviter std.Address, name string, profile string) { 30 // assert CallTx call. 31 std.AssertOriginCall() 32 // assert invited or paid. 33 caller := std.GetCallerAt(2) 34 if caller != std.GetOrigCaller() { 35 panic("should not happen") // because std.AssertOrigCall(). 36 } 37 sentCoins := std.GetOrigSend() 38 minCoin := std.Coin{"ugnot", minFee} 39 if inviter == "" { 40 // banker := std.GetBanker(std.BankerTypeOrigSend) 41 if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) { 42 if sentCoins[0].Amount > minFee*maxFeeMult { 43 panic("payment must not be greater than " + strconv.Itoa(int(minFee*maxFeeMult))) 44 } else { 45 // ok 46 } 47 } else { 48 panic("payment must not be less than " + strconv.Itoa(int(minFee))) 49 } 50 } else { 51 invitekey := inviter.String() + ":" + caller.String() 52 _, ok := invites.Get(invitekey) 53 if !ok { 54 panic("invalid invitation") 55 } 56 invites.Remove(invitekey) 57 } 58 // assert not already registered. 59 _, ok := name2User.Get(name) 60 if ok { 61 panic("name already registered") 62 } 63 _, ok = addr2User.Get(caller.String()) 64 if ok { 65 panic("address already registered") 66 } 67 // assert name is valid. 68 if !reName.MatchString(name) { 69 panic("invalid name: " + name + " (must be at least 6 characters, lowercase alphanumeric with underscore)") 70 } 71 // remainder of fees go toward invites. 72 invites := int(0) 73 if len(sentCoins) == 1 { 74 if sentCoins[0].Denom == "ugnot" && sentCoins[0].Amount >= minFee { 75 invites = int(sentCoins[0].Amount / minFee) 76 if inviter == "" && invites > 0 { 77 invites -= 1 78 } 79 } 80 } 81 // register. 82 counter++ 83 user := &users.User{ 84 Address: caller, 85 Name: name, 86 Profile: profile, 87 Number: counter, 88 Invites: invites, 89 Inviter: inviter, 90 } 91 name2User.Set(name, user) 92 addr2User.Set(caller.String(), user) 93 } 94 95 func Invite(invitee string) { 96 // assert CallTx call. 97 std.AssertOriginCall() 98 // get caller/inviter. 99 caller := std.GetCallerAt(2) 100 if caller != std.GetOrigCaller() { 101 panic("should not happen") // because std.AssertOrigCall(). 102 } 103 lines := strings.Split(invitee, "\n") 104 if caller == admin { 105 // nothing to do, all good 106 } else { 107 // ensure has invites. 108 userI, ok := addr2User.Get(caller.String()) 109 if !ok { 110 panic("user unknown") 111 } 112 user := userI.(*users.User) 113 if user.Invites <= 0 { 114 panic("user has no invite tokens") 115 } 116 user.Invites -= len(lines) 117 if user.Invites < 0 { 118 panic("user has insufficient invite tokens") 119 } 120 } 121 // for each line... 122 for _, line := range lines { 123 if line == "" { 124 continue // file bodies have a trailing newline. 125 } else if strings.HasPrefix(line, `//`) { 126 continue // comment 127 } 128 // record invite. 129 invitekey := string(caller) + ":" + string(line) 130 invites.Set(invitekey, true) 131 } 132 } 133 134 func GrantInvites(invites string) { 135 // assert CallTx call. 136 std.AssertOriginCall() 137 // assert admin. 138 caller := std.GetCallerAt(2) 139 if caller != std.GetOrigCaller() { 140 panic("should not happen") // because std.AssertOrigCall(). 141 } 142 if caller != admin { 143 panic("unauthorized") 144 } 145 // for each line... 146 lines := strings.Split(invites, "\n") 147 for _, line := range lines { 148 if line == "" { 149 continue // file bodies have a trailing newline. 150 } else if strings.HasPrefix(line, `//`) { 151 continue // comment 152 } 153 // parse name and invites. 154 var name string 155 var invites int 156 parts := strings.Split(line, ":") 157 if len(parts) == 1 { // short for :1. 158 name = parts[0] 159 invites = 1 160 } else if len(parts) == 2 { 161 name = parts[0] 162 invites_, err := strconv.Atoi(parts[1]) 163 if err != nil { 164 panic(err) 165 } 166 invites = int(invites_) 167 } else { 168 panic("should not happen") 169 } 170 // give invites. 171 userI, ok := name2User.Get(name) 172 if !ok { 173 // maybe address. 174 userI, ok = addr2User.Get(name) 175 if !ok { 176 panic("invalid user " + name) 177 } 178 } 179 user := userI.(*users.User) 180 user.Invites += invites 181 } 182 } 183 184 // Any leftover fees go toward invitations. 185 func SetMinFee(newMinFee int64) { 186 // assert CallTx call. 187 std.AssertOriginCall() 188 // assert admin caller. 189 caller := std.GetCallerAt(2) 190 if caller != admin { 191 panic("unauthorized") 192 } 193 // update global variables. 194 minFee = newMinFee 195 } 196 197 // This helps prevent fat finger accidents. 198 func SetMaxFeeMultiple(newMaxFeeMult int64) { 199 // assert CallTx call. 200 std.AssertOriginCall() 201 // assert admin caller. 202 caller := std.GetCallerAt(2) 203 if caller != admin { 204 panic("unauthorized") 205 } 206 // update global variables. 207 maxFeeMult = newMaxFeeMult 208 } 209 210 //---------------------------------------- 211 // Exposed public functions 212 213 func GetUserByName(name string) *users.User { 214 userI, ok := name2User.Get(name) 215 if !ok { 216 return nil 217 } 218 return userI.(*users.User) 219 } 220 221 func GetUserByAddress(addr std.Address) *users.User { 222 userI, ok := addr2User.Get(addr.String()) 223 if !ok { 224 return nil 225 } 226 return userI.(*users.User) 227 } 228 229 // unlike GetUserByName, input must be "@" prefixed for names. 230 func GetUserByAddressOrName(input users.AddressOrName) *users.User { 231 name, isName := input.GetName() 232 if isName { 233 return GetUserByName(name) 234 } 235 return GetUserByAddress(std.Address(input)) 236 } 237 238 func Resolve(input users.AddressOrName) std.Address { 239 name, isName := input.GetName() 240 if !isName { 241 return std.Address(input) // TODO check validity 242 } 243 user := GetUserByName(name) 244 return user.Address 245 } 246 247 //---------------------------------------- 248 // Constants 249 250 // NOTE: name length must be clearly distinguishable from a bech32 address. 251 var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`) 252 253 //---------------------------------------- 254 // Render main page 255 256 func Render(path string) string { 257 if path == "" { 258 return renderHome() 259 } else if len(path) >= 38 { // 39? 40? 260 if path[:2] != "g1" { 261 return "invalid address " + path 262 } 263 user := GetUserByAddress(std.Address(path)) 264 if user == nil { 265 // TODO: display basic information about account. 266 return "unknown address " + path 267 } 268 return user.Render() 269 } else { 270 user := GetUserByName(path) 271 if user == nil { 272 return "unknown username " + path 273 } 274 return user.Render() 275 } 276 } 277 278 func renderHome() string { 279 doc := "" 280 name2User.Iterate("", "", func(key string, value interface{}) bool { 281 user := value.(*users.User) 282 doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" 283 return false 284 }) 285 return doc 286 }