Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}