Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}