Search Apps Documentation Source Content File Folder Download Copy Actions Download

grc721.gno

9.88 Kb ยท 429 lines
  1package chunk
  2
  3import (
  4	"chain/runtime"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/demo/tokens/grc721"
  9	"gno.land/p/nt/avl"
 10	"gno.land/p/nt/ufmt"
 11)
 12
 13const metadataSeparator = "|"
 14
 15var (
 16	nft        = grc721.NewBasicNFT("Akkadia Chunk", "AKC")
 17	ownerIndex = avl.NewTree() // owner -> []uint64 (encoded chunkKeys)
 18	metadata   = avl.NewTree() // worldID -> map[uint64]string (encodedKey -> "worldType|hash")
 19)
 20
 21// encodeChunkKey packs worldID, x, y into uint64
 22// Layout: worldID (20 bits) | x (22 bits) | y (22 bits)
 23func encodeChunkKey(worldID uint32, x, y int32) uint64 {
 24	return (uint64(worldID) << 44) | (uint64(uint32(x)&0x3FFFFF) << 22) | uint64(uint32(y)&0x3FFFFF)
 25}
 26
 27// decodeChunkKey unpacks uint64 into worldID, x, y
 28func decodeChunkKey(encoded uint64) (uint32, int32, int32) {
 29	worldID := uint32(encoded >> 44)
 30	x := int32((encoded >> 22) & 0x3FFFFF)
 31	y := int32(encoded & 0x3FFFFF)
 32	// Sign extend if negative (bit 21 set)
 33	if x&0x200000 != 0 {
 34		x |= ^0x3FFFFF
 35	}
 36	if y&0x200000 != 0 {
 37		y |= ^0x3FFFFF
 38	}
 39	return worldID, x, y
 40}
 41
 42// ==================== GRC-721 ====================
 43
 44func Name() string {
 45	return nft.Name()
 46}
 47
 48func Symbol() string {
 49	return nft.Symbol()
 50}
 51
 52func TokenCount() int64 {
 53	return nft.TokenCount()
 54}
 55
 56func BalanceOf(user address) int64 {
 57	balance, err := nft.BalanceOf(user)
 58	if err != nil {
 59		panic("balanceOf failed: " + err.Error())
 60	}
 61
 62	return balance
 63}
 64
 65func OwnerOf(tokenID grc721.TokenID) address {
 66	owner, err := nft.OwnerOf(tokenID)
 67	if err != nil {
 68		panic("ownerOf failed: " + err.Error())
 69	}
 70
 71	return owner
 72}
 73
 74func IsApprovedForAll(owner, user address) bool {
 75	return nft.IsApprovedForAll(owner, user)
 76}
 77
 78func GetApproved(tokenID grc721.TokenID) address {
 79	addr, err := nft.GetApproved(tokenID)
 80	if err != nil {
 81		panic("getApproved failed: " + err.Error())
 82	}
 83
 84	return addr
 85}
 86
 87func Approve(cur realm, user address, tokenID grc721.TokenID) {
 88	err := nft.Approve(user, tokenID)
 89	if err != nil {
 90		panic("approve failed: " + err.Error())
 91	}
 92}
 93
 94func SetApprovalForAll(cur realm, user address, approved bool) {
 95	err := nft.SetApprovalForAll(user, approved)
 96	if err != nil {
 97		panic("setApprovalForAll failed: " + err.Error())
 98	}
 99}
100
101func TransferFrom(cur realm, from, to address, tokenID grc721.TokenID) {
102	err := nft.TransferFrom(from, to, tokenID)
103	if err != nil {
104		panic("transferFrom failed: " + err.Error())
105	}
106}
107
108func Mint(cur realm, to address, worldType string, worldID uint32, x string, y string, chunkHashKey string, chunkVerifier string) grc721.TokenID {
109	caller := runtime.PreviousRealm().Address()
110	assertIsAdmin(caller)
111	mustGetWorld(worldID)
112
113	worldIDStr := ufmt.Sprintf("%d", worldID)
114	chunkKey := worldIDStr + ":" + x + "_" + y
115	tokenID := grc721.TokenID(chunkKey)
116
117	// Parse x, y for owner index
118	xInt, errX := strconv.Atoi(x)
119	if errX != nil {
120		panic("invalid x: " + x)
121	}
122	yInt, errY := strconv.Atoi(y)
123	if errY != nil {
124		panic("invalid y: " + y)
125	}
126
127	err := nft.Mint(to, tokenID)
128	if err != nil {
129		panic("mint failed: " + err.Error())
130	}
131
132	addToOwnerIndex(to, worldID, int32(xInt), int32(yInt))
133	createMetadata(tokenID, worldType, worldIDStr, x, y, chunkHashKey)
134
135	if chunkVerifier != "" {
136		setChunkVerifierInternal(worldID, chunkKey, chunkVerifier)
137	}
138
139	return tokenID
140}
141
142func Burn(cur realm, tokenID grc721.TokenID) {
143	panic("burn not supported for chunk tokens")
144}
145
146// ==================== Owner Index ====================
147
148func ListTokensByOwner(owner address, page, count int) []string {
149	if page < 1 {
150		panic("page must be at least 1")
151	}
152	if count < 1 {
153		panic("count must be at least 1")
154	}
155	if count > listLimit {
156		panic("count exceeds listLimit")
157	}
158
159	value, exists := ownerIndex.Get(owner.String())
160	if !exists {
161		return []string{}
162	}
163
164	chunks := value.([]uint64)
165
166	// Pagination
167	start := (page - 1) * count
168	if start >= len(chunks) {
169		return []string{}
170	}
171	end := start + count
172	if end > len(chunks) {
173		end = len(chunks)
174	}
175
176	// Convert to tokenID strings
177	result := make([]string, 0, end-start)
178	for _, encoded := range chunks[start:end] {
179		worldID, x, y := decodeChunkKey(encoded)
180		tokenID := ufmt.Sprintf("%d:%d_%d", worldID, x, y)
181		result = append(result, tokenID)
182	}
183	return result
184}
185
186func addToOwnerIndex(owner address, worldID uint32, x, y int32) {
187	encoded := encodeChunkKey(worldID, x, y)
188	ownerKey := owner.String()
189
190	value, exists := ownerIndex.Get(ownerKey)
191	if !exists {
192		ownerIndex.Set(ownerKey, []uint64{encoded})
193		return
194	}
195	chunks := value.([]uint64)
196	chunks = append(chunks, encoded)
197	ownerIndex.Set(ownerKey, chunks)
198}
199
200func removeFromOwnerIndex(owner address, worldID uint32, x, y int32) {
201	encoded := encodeChunkKey(worldID, x, y)
202	ownerKey := owner.String()
203
204	value, exists := ownerIndex.Get(ownerKey)
205	if !exists {
206		return
207	}
208	chunks := value.([]uint64)
209	for i, c := range chunks {
210		if c == encoded {
211			chunks = append(chunks[:i], chunks[i+1:]...)
212			ownerIndex.Set(ownerKey, chunks)
213			return
214		}
215	}
216}
217
218func ListOwners(tokenIDs ...grc721.TokenID) []map[string]string {
219	result := []map[string]string{}
220
221	if len(tokenIDs) == 0 {
222		return result
223	}
224	if len(tokenIDs) > listLimit {
225		panic("tokenIDs exceeds listLimit")
226	}
227
228	for _, tokenID := range tokenIDs {
229		owner, err := nft.OwnerOf(tokenID)
230		if err != nil {
231			continue
232		}
233		result = append(result, map[string]string{"id": string(tokenID), "owner": string(owner)})
234	}
235
236	return result
237}
238
239// ==================== Metadata ====================
240
241// parseTokenID splits tokenID "worldID:x_y" into worldID and chunkKey (x_y)
242func parseTokenID(tokenID grc721.TokenID) (string, string) {
243	tokenIDStr := string(tokenID)
244	parts := strings.SplitN(tokenIDStr, ":", 2)
245	if len(parts) != 2 {
246		panic("invalid tokenID format: " + tokenIDStr)
247	}
248	return parts[0], parts[1]
249}
250
251// parseChunkKey splits chunkKey "x_y" into x and y
252func parseChunkKey(chunkKey string) (string, string) {
253	parts := strings.SplitN(chunkKey, "_", 2)
254	if len(parts) != 2 {
255		panic("invalid chunkKey format: " + chunkKey)
256	}
257	return parts[0], parts[1]
258}
259
260// getWorldMetadataMap returns the metadata map for a world, creating if needed
261func getWorldMetadataMap(worldID string, create bool) map[uint64]string {
262	if v, exists := metadata.Get(worldID); exists {
263		return v.(map[uint64]string)
264	}
265	if !create {
266		return nil
267	}
268	m := make(map[uint64]string)
269	metadata.Set(worldID, m)
270	return m
271}
272
273// buildFullMetadata reconstructs full metadata from encoded key and stored URI path
274func buildFullMetadata(worldID uint32, encodedKey uint64, uri string) map[string]string {
275	_, x, y := decodeChunkKey(encodedKey)
276
277	parts := strings.SplitN(uri, metadataSeparator, 2)
278	worldType := parts[0]
279	hash := ""
280	if len(parts) > 1 {
281		hash = parts[1]
282	}
283
284	worldIDStr := ufmt.Sprintf("%d", worldID)
285	tokenIDStr := ufmt.Sprintf("%d:%d_%d", worldID, x, y)
286
287	result := make(map[string]string)
288	result["id"] = tokenIDStr
289	result["worldId"] = worldIDStr
290	result["x"] = ufmt.Sprintf("%d", x)
291	result["y"] = ufmt.Sprintf("%d", y)
292	result["worldType"] = worldType
293	result["hash"] = hash
294
295	return result
296}
297
298func createMetadata(tokenID grc721.TokenID, worldType string, worldID string, x string, y string, hash string) {
299	worldMap := getWorldMetadataMap(worldID, true)
300
301	// Parse coordinates for encoded key
302	xInt, _ := strconv.Atoi(x)
303	yInt, _ := strconv.Atoi(y)
304	worldIDInt, _ := strconv.Atoi(worldID)
305	encodedKey := encodeChunkKey(uint32(worldIDInt), int32(xInt), int32(yInt))
306
307	if _, exists := worldMap[encodedKey]; exists {
308		panic("metadata already exists")
309	}
310
311	uri := worldType + metadataSeparator + hash
312	worldMap[encodedKey] = uri
313}
314
315func GetMetadata(tokenID grc721.TokenID) map[string]string {
316	worldIDStr, chunkKey := parseTokenID(tokenID)
317	worldMap := getWorldMetadataMap(worldIDStr, false)
318	if worldMap == nil {
319		return nil
320	}
321
322	// Parse coordinates for encoded key
323	xStr, yStr := parseChunkKey(chunkKey)
324	xInt, _ := strconv.Atoi(xStr)
325	yInt, _ := strconv.Atoi(yStr)
326	worldIDInt, _ := strconv.Atoi(worldIDStr)
327	encodedKey := encodeChunkKey(uint32(worldIDInt), int32(xInt), int32(yInt))
328
329	uri, exists := worldMap[encodedKey]
330	if !exists {
331		return nil
332	}
333	return buildFullMetadata(uint32(worldIDInt), encodedKey, uri)
334}
335
336func GetWorldMetadataSize(worldID uint32) int {
337	worldIDStr := ufmt.Sprintf("%d", worldID)
338	worldMap := getWorldMetadataMap(worldIDStr, false)
339	if worldMap == nil {
340		return 0
341	}
342	return len(worldMap)
343}
344
345func ListMetadataByWorld(worldID uint32, page, count int) []map[string]string {
346	if page < 1 {
347		panic("page must be at least 1")
348	}
349	if count < 1 {
350		panic("count must be at least 1")
351	}
352	if count > listLimit {
353		panic("count exceeds listLimit")
354	}
355
356	worldIDStr := ufmt.Sprintf("%d", worldID)
357	worldMap := getWorldMetadataMap(worldIDStr, false)
358	if worldMap == nil {
359		return []map[string]string{}
360	}
361
362	offset := (page - 1) * count
363	idx := 0
364	result := make([]map[string]string, 0, count)
365
366	for encodedKey, uri := range worldMap {
367		if idx < offset {
368			idx++
369			continue
370		}
371		result = append(result, buildFullMetadata(worldID, encodedKey, uri))
372		if len(result) >= count {
373			break
374		}
375		idx++
376	}
377
378	return result
379}
380
381func GetOwnerTokenSize(owner address) int {
382	value, exists := ownerIndex.Get(owner.String())
383	if !exists {
384		return 0
385	}
386	return len(value.([]uint64))
387}
388
389func ListMetadataByOwner(owner address, page, count int) []map[string]string {
390	if page < 1 {
391		panic("page must be at least 1")
392	}
393	if count < 1 {
394		panic("count must be at least 1")
395	}
396	if count > listLimit {
397		panic("count exceeds listLimit")
398	}
399
400	value, exists := ownerIndex.Get(owner.String())
401	if !exists {
402		return []map[string]string{}
403	}
404
405	chunks := value.([]uint64)
406
407	// Pagination
408	start := (page - 1) * count
409	if start >= len(chunks) {
410		return []map[string]string{}
411	}
412	end := start + count
413	if end > len(chunks) {
414		end = len(chunks)
415	}
416
417	// Get metadata for paginated slice
418	result := make([]map[string]string, 0, end-start)
419	for _, encoded := range chunks[start:end] {
420		worldID, x, y := decodeChunkKey(encoded)
421		tokenID := grc721.TokenID(ufmt.Sprintf("%d:%d_%d", worldID, x, y))
422		data := GetMetadata(tokenID)
423		if data != nil {
424			result = append(result, data)
425		}
426	}
427
428	return result
429}