equipment.gno
10.56 Kb · 394 lines
1package equipment
2
3import (
4 "chain/runtime/unsafe"
5 "net/url"
6 "strconv"
7 "time"
8
9 "gno.land/p/akkadia/beta/accesscontrol"
10 "gno.land/p/akkadia/beta/grc721"
11 "gno.land/p/nt/bptree/v0"
12 "gno.land/r/akkadia/beta/admin"
13)
14
15const (
16 definitionIDKey = "definitionId"
17 tokenIDKey = "tokenId"
18 createdAtKey = "created_at"
19 maxEquipmentTokenID uint64 = 18446744073709551615
20)
21
22var (
23 equipmentToken = grc721.NewBasicNFT("Akkadia Equipment", "AEQP")
24 equipmentMetadata *bptree.BPTree
25 ownerTokenIDs *bptree.BPTree
26 latestTokenID uint64
27)
28
29func init() { resetEquipmentState() }
30
31func resetEquipmentState() {
32 equipmentToken = grc721.NewBasicNFT("Akkadia Equipment", "AEQP")
33 equipmentMetadata = bptree.NewBPTree32()
34 ownerTokenIDs = bptree.NewBPTree32()
35 minterRealms = bptree.NewBPTree32()
36 listLimit = 100
37 latestTokenID = 0
38}
39
40func Mint(cur realm, recipient address, definitionID uint32, attrKeys, attrValues string) uint64 {
41 accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
42 assertEquipmentNoCoins()
43 return mintEquipment(cross(cur), recipient, definitionID, attrKeys, attrValues)
44}
45
46func MintFromRealm(cur realm, recipient address, definitionID uint32, attrKeys, attrValues string) uint64 {
47 mustGetMinterRealmCaller(0, cur)
48 if unsafe.OriginCaller() != recipient {
49 panic("equipment recipient must be origin caller")
50 }
51 return mintEquipment(cross(cur), recipient, definitionID, attrKeys, attrValues)
52}
53
54func GetMetadata(tokenID uint64) map[string]string {
55 return copyStringMap(mustGetEquipmentMetadata(tokenID))
56}
57
58func SetMetadata(cur realm, tokenID uint64, attrKeys, attrValues string) {
59 accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
60 assertEquipmentNoCoins()
61 setEquipmentMetadata(tokenID, attrKeys, attrValues)
62}
63
64func SetMetadataFromRealm(cur realm, tokenID uint64, attrKeys, attrValues string) {
65 mustGetMinterRealmCaller(0, cur)
66 if unsafe.OriginCaller() != OwnerOf(tokenID) {
67 panic("equipment owner must be origin caller")
68 }
69 setEquipmentMetadata(tokenID, attrKeys, attrValues)
70}
71
72func GetOwnedCount(owner address) int {
73 if !owner.IsValid() {
74 panic("invalid equipment owner")
75 }
76 ids, found := getOwnerTokenIDs(owner)
77 if !found {
78 return 0
79 }
80 return ids.Size()
81}
82
83func ListByOwner(owner address, page, count int) []map[string]string {
84 if !owner.IsValid() {
85 panic("invalid equipment owner")
86 }
87 assertEquipmentListPageCount(page, count)
88 ids, found := getOwnerTokenIDs(owner)
89 if !found {
90 return []map[string]string{}
91 }
92 result := []map[string]string{}
93 ids.IterateByOffset((page-1)*count, count, func(key string, _ any) bool {
94 tokenID := parseUint64Key(key)
95 row := GetMetadata(tokenID)
96 row[tokenIDKey] = strconv.FormatUint(tokenID, 10)
97 result = append(result, row)
98 return false
99 })
100 return result
101}
102
103func Name() string { return equipmentToken.Name() }
104func Symbol() string { return equipmentToken.Symbol() }
105
106func OwnerOf(tokenID uint64) address {
107 owner, err := equipmentToken.OwnerOf(equipmentTokenID(tokenID))
108 if err != nil {
109 panic("equipment owner query failed: " + err.Error())
110 }
111 return owner
112}
113
114func Has(tokenID uint64) bool {
115 _, err := equipmentToken.OwnerOf(equipmentTokenID(tokenID))
116 return err == nil
117}
118
119func GetLatestTokenID() uint64 {
120 return latestTokenID
121}
122
123func BalanceOf(owner address) int64 {
124 if !owner.IsValid() {
125 panic("invalid equipment owner")
126 }
127 balance, err := equipmentToken.BalanceOf(owner)
128 if err != nil {
129 panic("equipment balance query failed: " + err.Error())
130 }
131 return balance
132}
133
134func GetApproved(tokenID uint64) address {
135 approved, err := equipmentToken.GetApproved(equipmentTokenID(tokenID))
136 if err != nil {
137 panic("equipment approval query failed: " + err.Error())
138 }
139 return approved
140}
141
142func IsApprovedForAll(owner, operator address) bool {
143 return equipmentToken.IsApprovedForAll(owner, operator)
144}
145
146func Approve(cur realm, operator address, tokenID uint64) {
147 caller := accesscontrol.MustGetUserCaller(0, cur)
148 assertEquipmentNoCoins()
149 if err := equipmentToken.Approve(caller, operator, equipmentTokenID(tokenID)); err != nil {
150 panic("equipment approval failed: " + err.Error())
151 }
152}
153
154func SetApprovalForAll(cur realm, operator address, approved bool) {
155 caller := accesscontrol.MustGetUserCaller(0, cur)
156 assertEquipmentNoCoins()
157 if err := equipmentToken.SetApprovalForAll(caller, operator, approved); err != nil {
158 panic("equipment operator approval failed: " + err.Error())
159 }
160}
161
162func TransferFrom(cur realm, from, to address, tokenID uint64) {
163 caller := accesscontrol.MustGetUserCaller(0, cur)
164 assertEquipmentNoCoins()
165 if err := equipmentToken.TransferFrom(caller, from, to, equipmentTokenID(tokenID)); err != nil {
166 panic("equipment transfer failed: " + err.Error())
167 }
168 if from != to {
169 removeOwnerTokenID(from, tokenID)
170 addOwnerTokenID(to, tokenID)
171 }
172}
173
174func Burn(cur realm, tokenID uint64) {
175 caller := accesscontrol.MustGetUserCaller(0, cur)
176 assertEquipmentNoCoins()
177 owner := OwnerOf(tokenID)
178 approved, err := equipmentToken.GetApproved(equipmentTokenID(tokenID))
179 if caller != owner && !equipmentToken.IsApprovedForAll(owner, caller) && (err != nil || approved != caller) {
180 panic("equipment owner or approved operator required")
181 }
182 burnEquipment(tokenID, owner)
183}
184
185func BurnFromRealm(cur realm, tokenID uint64) {
186 mustGetMinterRealmCaller(0, cur)
187 owner := OwnerOf(tokenID)
188 if unsafe.OriginCaller() != owner {
189 panic("equipment owner must be origin caller")
190 }
191 burnEquipment(tokenID, owner)
192}
193
194func mintEquipment(cur realm, recipient address, definitionID uint32, attrKeys, attrValues string) uint64 {
195 if !recipient.IsValid() || recipient == cur.Address() {
196 panic("invalid equipment recipient")
197 }
198 attrs := parseEquipmentAttributes(attrKeys, attrValues)
199 tokenID := allocateTokenID()
200 if err := equipmentToken.Mint(recipient, equipmentTokenID(tokenID)); err != nil {
201 panic("equipment mint failed: " + err.Error())
202 }
203 attrs[definitionIDKey] = strconv.FormatUint(uint64(definitionID), 10)
204 attrs[createdAtKey] = strconv.FormatInt(time.Now().Unix(), 10)
205 equipmentMetadata.Set(uint64Key(tokenID), attrs)
206 addOwnerTokenID(recipient, tokenID)
207 return tokenID
208}
209
210func allocateTokenID() uint64 {
211 if latestTokenID == maxEquipmentTokenID {
212 panic("equipment token id exhausted")
213 }
214 latestTokenID++
215 return latestTokenID
216}
217
218func mustGetEquipmentMetadata(tokenID uint64) map[string]string {
219 value := equipmentMetadata.Get(uint64Key(tokenID))
220 if value == nil {
221 panic("equipment metadata not found: " + strconv.FormatUint(tokenID, 10))
222 }
223 return value.(map[string]string)
224}
225
226func setEquipmentMetadata(tokenID uint64, attrKeys, attrValues string) {
227 current := mustGetEquipmentMetadata(tokenID)
228 attrs := parseEquipmentAttributes(attrKeys, attrValues)
229 attrs[definitionIDKey] = current[definitionIDKey]
230 attrs[createdAtKey] = current[createdAtKey]
231 equipmentMetadata.Set(uint64Key(tokenID), attrs)
232}
233
234func parseEquipmentAttributes(attrKeys, attrValues string) map[string]string {
235 attrs := parseAttributesCSV(attrKeys, attrValues)
236 if _, found := attrs[definitionIDKey]; found {
237 panic("reserved key: " + definitionIDKey)
238 }
239 if _, found := attrs[tokenIDKey]; found {
240 panic("reserved key: " + tokenIDKey)
241 }
242 if _, found := attrs[createdAtKey]; found {
243 panic("reserved key: " + createdAtKey)
244 }
245 return attrs
246}
247
248func burnEquipment(tokenID uint64, owner address) {
249 if err := equipmentToken.Burn(equipmentTokenID(tokenID)); err != nil {
250 panic("equipment burn failed: " + err.Error())
251 }
252 if _, removed := equipmentMetadata.Remove(uint64Key(tokenID)); !removed {
253 panic("equipment metadata not found: " + strconv.FormatUint(tokenID, 10))
254 }
255 removeOwnerTokenID(owner, tokenID)
256}
257
258func mustGetMinterRealmCaller(_ int, rlm realm) address {
259 accesscontrol.AssertCurrentRealm(0, rlm)
260 prev := rlm.Previous()
261 if prev.IsUserCall() {
262 panic("user calls not allowed: function must be called by a realm")
263 }
264 caller := prev.Address()
265 if !IsMinterRealm(caller) {
266 panic("minter realm required")
267 }
268 return caller
269}
270
271func assertEquipmentNoCoins() {
272 if len(unsafe.OriginSend()) != 0 {
273 panic("attached coins not allowed")
274 }
275}
276
277func assertEquipmentListPageCount(page, count int) {
278 if page < 1 {
279 panic("page must be at least 1")
280 }
281 if count < 1 {
282 panic("count must be at least 1")
283 }
284 if count > listLimit {
285 panic("count exceeds listLimit (max: " + strconv.Itoa(listLimit) + ")")
286 }
287}
288
289func addOwnerTokenID(owner address, tokenID uint64) {
290 value := ownerTokenIDs.Get(owner.String())
291 var ids *bptree.BPTree
292 if value != nil {
293 ids = value.(*bptree.BPTree)
294 } else {
295 ids = bptree.NewBPTree32()
296 ownerTokenIDs.Set(owner.String(), ids)
297 }
298 ids.Set(uint64Key(tokenID), true)
299}
300
301func removeOwnerTokenID(owner address, tokenID uint64) {
302 ids, found := getOwnerTokenIDs(owner)
303 if !found {
304 return
305 }
306 ids.Remove(uint64Key(tokenID))
307}
308
309func getOwnerTokenIDs(owner address) (*bptree.BPTree, bool) {
310 value := ownerTokenIDs.Get(owner.String())
311 if value == nil {
312 return nil, false
313 }
314 return value.(*bptree.BPTree), true
315}
316
317func equipmentTokenID(tokenID uint64) grc721.TokenID {
318 return grc721.TokenID(strconv.FormatUint(tokenID, 10))
319}
320
321func uint64Key(id uint64) string {
322 s := strconv.FormatUint(id, 10)
323 if len(s) >= 20 {
324 return s
325 }
326 return "00000000000000000000"[len(s):] + s
327}
328
329func parseUint64Key(key string) uint64 {
330 id, err := strconv.ParseUint(key, 10, 64)
331 if err != nil {
332 panic("invalid uint64 key: " + key)
333 }
334 return id
335}
336
337func copyStringMap(source map[string]string) map[string]string {
338 result := map[string]string{}
339 for key, value := range source {
340 result[key] = value
341 }
342 return result
343}
344
345// parseAttributesCSV accepts comma-separated keys and URL-encoded values.
346// Empty keys and values produce an empty attribute map so mint can store
347// definitionId and created_at alone.
348func parseAttributesCSV(keys, values string) map[string]string {
349 if keys == "" && values == "" {
350 return map[string]string{}
351 }
352
353 result := map[string]string{}
354 keyStart, valStart := 0, 0
355 keyIdx, valIdx := 0, 0
356 for {
357 for keyIdx < len(keys) && keys[keyIdx] != ',' {
358 keyIdx++
359 }
360 for valIdx < len(values) && values[valIdx] != ',' {
361 valIdx++
362 }
363
364 key := keys[keyStart:keyIdx]
365 if key == "" {
366 panic("attributes key must be not empty")
367 }
368 if _, found := result[key]; found {
369 panic("attributes key duplicated: " + key)
370 }
371
372 value, err := url.PathUnescape(values[valStart:valIdx])
373 if err != nil {
374 panic("attributes value URL decode failed: " + key)
375 }
376 result[key] = value
377
378 keyEnd := keyIdx >= len(keys)
379 valEnd := valIdx >= len(values)
380 if keyEnd != valEnd {
381 panic("attributes keys and values count mismatch")
382 }
383 if keyEnd {
384 break
385 }
386
387 keyIdx++
388 valIdx++
389 keyStart = keyIdx
390 valStart = valIdx
391 }
392
393 return result
394}