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  }