profile.gno
5.07 Kb · 234 lines
1// Package profile is the Shoal sailor card. One address has one username.
2// This realm holds no ugnot. It does not mint FISH and it does not mint a fish NFT.
3// Deployed on onyx-1 at gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/shoal/profile.
4// The older profile package at the founder root stays as it was.
5package profile
6
7import (
8 "chain"
9 "chain/runtime/unsafe"
10)
11
12type card struct {
13 username string
14 country string
15 crest string
16}
17
18var (
19 byAddr map[address]*card
20 byName map[string]address
21 admin address
22 adminSet bool
23 extraCountry map[string]bool
24 extraCrest map[string]bool
25)
26
27// SetProfile stores this caller's sailor card. The caller does not need a fish NFT.
28// A later fact is a new function. This one keeps username, country, and crest.
29func SetProfile(cur realm, username, country, crest string) {
30 caller := requireUser(cur)
31 takeExact(0)
32 name := cleanUser(username)
33 if name == "" {
34 panic("profile: name")
35 }
36 if !knownCountry(country) {
37 panic("profile: country")
38 }
39 if !knownCrest(crest) {
40 panic("profile: crest")
41 }
42 ensure()
43 if owner, ok := byName[name]; ok && owner != caller {
44 panic("profile: taken")
45 }
46 prev := byAddr[caller]
47 if prev != nil && prev.username == name && prev.country == country && prev.crest == crest {
48 return
49 }
50 if prev != nil && prev.username != name {
51 delete(byName, prev.username)
52 }
53 byAddr[caller] = &card{username: name, country: country, crest: crest}
54 byName[name] = caller
55 chain.Emit("SetProfile", "who", caller.String(), "username", name, "country", country, "crest", crest)
56}
57
58// Version names this local package shape. The live profile realm is unchanged.
59func Version() string { return "profile-1" }
60
61// Admin is empty until ClaimAdmin. SetProfile does not claim it.
62func Admin() address { return admin }
63
64// ClaimAdmin records the first caller. A later caller cannot replace them here.
65func ClaimAdmin(cur realm) {
66 caller := requireUser(cur)
67 takeExact(0)
68 if adminSet {
69 panic("profile: admin")
70 }
71 admin = caller
72 adminSet = true
73 chain.Emit("ClaimAdmin", "who", caller.String())
74}
75
76// AddCountry lets the admin accept one more country name. Built-in names stay.
77func AddCountry(cur realm, name string) {
78 caller := requireUser(cur)
79 takeExact(0)
80 if !adminSet || caller != admin {
81 panic("profile: admin")
82 }
83 if !placeName(name) || knownCountry(name) {
84 panic("profile: country")
85 }
86 if extraCountry == nil {
87 extraCountry = make(map[string]bool)
88 }
89 extraCountry[name] = true
90 chain.Emit("AddCountry", "name", name)
91}
92
93// AddCrest lets the admin accept one more crest name. Built-in names stay.
94func AddCrest(cur realm, name string) {
95 caller := requireUser(cur)
96 takeExact(0)
97 if !adminSet || caller != admin {
98 panic("profile: admin")
99 }
100 if !placeName(name) || knownCrest(name) {
101 panic("profile: crest")
102 }
103 if extraCrest == nil {
104 extraCrest = make(map[string]bool)
105 }
106 extraCrest[name] = true
107 chain.Emit("AddCrest", "name", name)
108}
109
110func Has(cur realm, who address) bool {
111 takeExact(0)
112 return cardOf(who) != nil
113}
114
115func UsernameOf(cur realm, who address) string {
116 takeExact(0)
117 body := cardOf(who)
118 if body == nil {
119 return ""
120 }
121 return body.username
122}
123
124func CountryOf(cur realm, who address) string {
125 takeExact(0)
126 body := cardOf(who)
127 if body == nil {
128 return ""
129 }
130 return body.country
131}
132
133func CrestOf(cur realm, who address) string {
134 takeExact(0)
135 body := cardOf(who)
136 if body == nil {
137 return ""
138 }
139 return body.crest
140}
141
142func AddressOf(cur realm, username string) address {
143 takeExact(0)
144 name := cleanUser(username)
145 if name == "" || byName == nil {
146 return address("")
147 }
148 return byName[name]
149}
150
151func requireUser(cur realm) address {
152 if !cur.Previous().IsUserCall() {
153 panic("profile: eoa")
154 }
155 return cur.Previous().Address()
156}
157
158func takeExact(n int64) {
159 sent := unsafe.OriginSend()
160 if n == 0 {
161 if len(sent) != 0 {
162 panic("profile: payment")
163 }
164 return
165 }
166 panic("profile: payment")
167}
168
169func ensure() {
170 if byAddr == nil {
171 byAddr = make(map[address]*card)
172 }
173 if byName == nil {
174 byName = make(map[string]address)
175 }
176}
177
178func cardOf(who address) *card {
179 if byAddr == nil || !who.IsValid() {
180 return nil
181 }
182 return byAddr[who]
183}
184
185func cleanUser(name string) string {
186 for len(name) > 0 && name[0] == ' ' {
187 name = name[1:]
188 }
189 for len(name) > 0 && name[len(name)-1] == ' ' {
190 name = name[:len(name)-1]
191 }
192 if len(name) < 3 || len(name) > 16 {
193 return ""
194 }
195 out := ""
196 for _, c := range name {
197 if c >= 'A' && c <= 'Z' {
198 c = c - 'A' + 'a'
199 }
200 if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
201 return ""
202 }
203 out += string(c)
204 }
205 return out
206}
207
208func knownCountry(s string) bool {
209 switch s {
210 case "Sea", "Vietnam", "Korea", "Japan", "Brazil", "France", "Nigeria", "Canada", "Australia":
211 return true
212 }
213 return extraCountry != nil && extraCountry[s]
214}
215
216func knownCrest(s string) bool {
217 switch s {
218 case "wave", "reef", "moon", "sun", "fang", "shell":
219 return true
220 }
221 return extraCrest != nil && extraCrest[s]
222}
223
224func placeName(s string) bool {
225 if len(s) < 2 || len(s) > 24 {
226 return false
227 }
228 for _, c := range s {
229 if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') {
230 return false
231 }
232 }
233 return true
234}