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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}