grc721.gno
8.22 Kb · 318 lines
1package chunk
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/p/akkadia/v0/accesscontrol"
8 "gno.land/p/akkadia/v0/grc721"
9 "gno.land/r/akkadia/v0/admin"
10)
11
12const metadataSeparator = "|"
13
14// ==================== GRC-721 ====================
15
16func Name() string {
17 assertMigrationStateAvailable()
18 return nftStore.Name()
19}
20
21func Symbol() string {
22 assertMigrationStateAvailable()
23 return nftStore.Symbol()
24}
25
26func TokenCount() int64 {
27 assertMigrationStateAvailable()
28 return nftStore.TokenCount()
29}
30
31func BalanceOf(user address) int64 {
32 assertMigrationStateAvailable()
33 balance, err := nftStore.BalanceOf(user)
34 if err != nil {
35 panic("balanceOf failed: " + err.Error())
36 }
37
38 return balance
39}
40
41func OwnerOf(tokenID grc721.TokenID) address {
42 assertMigrationStateAvailable()
43 owner, found := nftStore.OwnerOfSafe(tokenID)
44 if !found {
45 _, err := nftStore.OwnerOf(tokenID)
46 panic("ownerOf failed: " + err.Error())
47 }
48
49 return owner
50}
51
52func IsApprovedForAll(owner, user address) bool {
53 assertMigrationStateAvailable()
54 return nftStore.IsApprovedForAll(owner, user)
55}
56
57func GetApproved(tokenID grc721.TokenID) address {
58 assertMigrationStateAvailable()
59 addr, err := nftStore.GetApproved(tokenID)
60 if err != nil {
61 panic("getApproved failed: " + err.Error())
62 }
63
64 return addr
65}
66
67func Approve(cur realm, user address, tokenID grc721.TokenID) {
68 assertNotFrozen()
69 caller := accesscontrol.MustGetUserCaller(0, cur)
70 err := nftStore.Approve(caller, user, tokenID)
71 if err != nil {
72 panic("approve failed: " + err.Error())
73 }
74}
75
76func SetApprovalForAll(cur realm, user address, approved bool) {
77 assertNotFrozen()
78 caller := accesscontrol.MustGetUserCaller(0, cur)
79 err := nftStore.SetApprovalForAll(caller, user, approved)
80 if err != nil {
81 panic("setApprovalForAll failed: " + err.Error())
82 }
83}
84
85func TransferFrom(cur realm, from, to address, tokenID grc721.TokenID) {
86 assertNotFrozen()
87 caller := accesscontrol.MustGetUserCaller(0, cur)
88 worldID, _, _ := parseChunkKey(string(tokenID))
89 err := nftStore.Transfer(caller, from, to, tokenID, worldID)
90 if err != nil {
91 panic("transferFrom failed: " + err.Error())
92 }
93
94 chunkAuthzRBAC.DeleteEntity(tokenID.String())
95}
96
97func Mint(cur realm, to address, worldType string, worldID uint32, x, y int, chunkHashKey string, chunkVerifier string) grc721.TokenID {
98 assertNotFrozen()
99 accesscontrol.AssertIsAdminOrOperator(0, cur, admin.IsAdmin, admin.IsOperator)
100 worldStore.AssertExists(worldID)
101
102 chunkKey := buildChunkKey(worldID, x, y)
103 tokenID := grc721.TokenID(chunkKey)
104 metadata := buildChunkMetadataValue(worldType, chunkHashKey)
105 err := nftStore.Mint(to, tokenID, worldID, metadata)
106 if err != nil {
107 panic("mint failed for " + chunkKey + ": " + err.Error())
108 }
109
110 if chunkVerifier != "" {
111 verifierStore.Set(worldID, chunkKey, chunkVerifier)
112 }
113
114 return tokenID
115}
116
117func MintBatch(cur realm, to address, worldType string, worldID uint32, xs string, ys string, chunkHashKeys string, chunkVerifiers string) {
118 assertNotFrozen()
119 accesscontrol.AssertIsAdminOrOperator(0, cur, admin.IsAdmin, admin.IsOperator)
120 worldStore.AssertExists(worldID)
121
122 if xs == "" {
123 panic("xs must not be empty")
124 }
125 if ys == "" {
126 panic("ys must not be empty")
127 }
128 if chunkHashKeys == "" {
129 panic("chunkHashKeys must not be empty")
130 }
131
132 xStart, yStart, hStart, vStart := 0, 0, 0, 0
133 xIdx, yIdx, hIdx, vIdx := 0, 0, 0, 0
134
135 // Stream each parsed row directly into storage instead of first appending all
136 // rows to temporary slices. Large batch mints are gas sensitive, and Gno
137 // transaction rollback preserves atomicity if a later row panics.
138 for {
139 for xIdx < len(xs) && xs[xIdx] != ',' {
140 xIdx++
141 }
142 for yIdx < len(ys) && ys[yIdx] != ',' {
143 yIdx++
144 }
145 for hIdx < len(chunkHashKeys) && chunkHashKeys[hIdx] != ',' {
146 hIdx++
147 }
148 for vIdx < len(chunkVerifiers) && chunkVerifiers[vIdx] != ',' {
149 vIdx++
150 }
151
152 x := xs[xStart:xIdx]
153 y := ys[yStart:yIdx]
154 hashKey := chunkHashKeys[hStart:hIdx]
155 verifier := ""
156 if vStart <= vIdx && vIdx <= len(chunkVerifiers) {
157 verifier = chunkVerifiers[vStart:vIdx]
158 }
159
160 if x == "" {
161 panic("empty x not allowed")
162 }
163 if y == "" {
164 panic("empty y not allowed")
165 }
166 if hashKey == "" {
167 panic("empty key not allowed")
168 }
169
170 xInt, errX := strconv.Atoi(x)
171 if errX != nil {
172 panic("invalid x: " + x)
173 }
174 yInt, errY := strconv.Atoi(y)
175 if errY != nil {
176 panic("invalid y: " + y)
177 }
178
179 xEnd := xIdx >= len(xs)
180 yEnd := yIdx >= len(ys)
181 hEnd := hIdx >= len(chunkHashKeys)
182 if xEnd != yEnd || xEnd != hEnd {
183 panic("xs, ys and haskeys count mismatch")
184 }
185
186 chunkKey := buildChunkKey(worldID, xInt, yInt)
187 tokenID := grc721.TokenID(chunkKey)
188 metadata := buildChunkMetadataValue(worldType, hashKey)
189 err := nftStore.Mint(to, tokenID, worldID, metadata)
190 if err != nil {
191 panic("mint failed for " + chunkKey + ": " + err.Error())
192 }
193
194 if verifier != "" {
195 verifierStore.Set(worldID, chunkKey, verifier)
196 }
197
198 if xEnd {
199 break
200 }
201
202 xIdx++
203 yIdx++
204 hIdx++
205 vIdx++
206 xStart = xIdx
207 yStart = yIdx
208 hStart = hIdx
209 vStart = vIdx
210 }
211}
212
213func Burn(cur realm, tokenID grc721.TokenID) {
214 assertNotFrozen()
215 panic("burn not supported for chunk tokens")
216}
217
218func ListOwners(tokenIDs ...grc721.TokenID) []map[string]string {
219 assertMigrationStateAvailable()
220 assertListLimit("tokenIDs", len(tokenIDs))
221 return nftStore.ListOwners(tokenIDs...)
222}
223
224// ==================== Metadata ====================
225
226func SetChunkMetadata(cur realm, worldType string, worldID uint32, x, y int32, hash string) {
227 assertNotFrozen()
228 accesscontrol.AssertIsAdminOrOperator(0, cur, admin.IsAdmin, admin.IsOperator)
229 worldStore.AssertExists(worldID)
230 tokenID := grc721.TokenID(buildChunkKey(worldID, int(x), int(y)))
231 metadata := buildChunkMetadataValue(worldType, hash)
232 nftStore.SetChunkMetadata(worldID, tokenID, metadata)
233}
234
235func GetChunkMetadata(chunkKey string) map[string]string {
236 assertMigrationStateAvailable()
237 worldID, _, _ := parseChunkKey(chunkKey)
238 row := nftStore.GetChunkMetadata(worldID, grc721.TokenID(chunkKey))
239 if row == nil {
240 return nil
241 }
242 return buildMetadataResponse(row)
243}
244
245func GetChunkMetadataSizeByWorld(worldID uint32) int {
246 assertMigrationStateAvailable()
247 return nftStore.WorldMetadataSize(worldID)
248}
249
250// ListChunkMetadataByKeys retrieves metadata for specific chunk keys.
251// Each key format: "worldId:x_y" (same as tokenID).
252func ListChunkMetadataByKeys(chunkKeys ...string) []map[string]string {
253 assertMigrationStateAvailable()
254 assertListLimit("chunkKeys", len(chunkKeys))
255
256 result := []map[string]string{}
257 for _, chunkKey := range chunkKeys {
258 worldID, _, _ := parseChunkKey(chunkKey)
259 row := nftStore.GetChunkMetadata(worldID, grc721.TokenID(chunkKey))
260 if row != nil {
261 result = append(result, buildMetadataResponse(row))
262 }
263 }
264 return result
265}
266
267func buildChunkMetadataValue(worldType string, hash string) string {
268 // Keep the persisted chunk metadata as one encoded string for gas efficiency.
269 // Public query helpers expand it back into a map-shaped response.
270 return worldType + metadataSeparator + hash
271}
272
273func buildMetadataResponse(row map[string]string) map[string]string {
274 chunkKey := row["id"]
275 value := row["metadata"]
276 worldID, x, y := parseChunkKey(chunkKey)
277
278 parts := strings.SplitN(value, metadataSeparator, 2)
279 worldType := parts[0]
280 hash := ""
281 if len(parts) > 1 {
282 hash = parts[1]
283 }
284
285 return map[string]string{
286 "id": chunkKey,
287 "worldId": strconv.FormatUint(uint64(worldID), 10),
288 "x": strconv.Itoa(x),
289 "y": strconv.Itoa(y),
290 "worldType": worldType,
291 "hash": hash,
292 }
293}
294
295func ListChunkMetadataByWorld(worldID uint32, page int, count int) []map[string]string {
296 assertMigrationStateAvailable()
297 assertListPageCount(page, count)
298 rows := nftStore.ListChunkMetadataByWorld(worldID, page, count)
299 result := []map[string]string{}
300 for _, row := range rows {
301 result = append(result, buildMetadataResponse(row))
302 }
303 return result
304}
305
306// ==================== Owner Index ====================
307
308// ListChunkKeysByOwner returns chunk keys owned by owner in page/count order.
309func ListChunkKeysByOwner(worldID uint32, owner address, page, count int) []string {
310 assertMigrationStateAvailable()
311 assertListPageCount(page, count)
312 return nftStore.ListTokenIDsByOwner(worldID, owner, page, count)
313}
314
315func GetOwnerTokenSize(worldID uint32, owner address) int {
316 assertMigrationStateAvailable()
317 return nftStore.OwnerTokenSize(worldID, owner)
318}