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}