Search Apps Documentation Source Content File Folder Download Copy Actions Download

nft_store.gno

8.46 Kb · 295 lines
  1package chunk
  2
  3import (
  4	"gno.land/p/akkadia/v0/ds/btree"
  5	"gno.land/p/akkadia/v0/ds/btreeset"
  6	"gno.land/p/akkadia/v0/grc721"
  7	"gno.land/p/nt/bptree/v0"
  8)
  9
 10var nft = grc721.NewBasicNFT("Akkadia Chunk", "AKC")
 11var nftStore = newNFTStore(nft)
 12
 13// NFTStore owns chunk token storage access and token-owned secondary indexes.
 14//
 15// Data shape:
 16// - token: GRC-721 token state
 17// - metadataByWorldID: worldID(uint32) -> *StringBTree(tokenID(string) -> metadata(string))
 18// - ownerIndexByWorld: worldID(string) -> *BPTree(owner(address string) -> *StringBTreeSet(tokenID))
 19//
 20// Responsibilities:
 21// - delegate GRC-721 token reads and writes to the package-owned token
 22// - keep chunk metadata and owner-token indexes in sync with token mutations
 23// - protect store-owned invariants such as duplicate metadata rows for the same token ID
 24// - return newly allocated result slices and maps from public read methods
 25//
 26// Non-responsibilities:
 27// - validate external parameter shape such as world type, hash, verifier, coordinates, or pagination
 28// - perform auth, freeze, migration, payment, or event handling
 29// - check world existence or higher-level ownership rules outside token transfer semantics
 30// - build or parse token, chunk, or coordinate keys; public exported functions must pass canonical raw keys
 31// - parse user-facing encoded input; public exported functions must pass canonical values
 32type NFTStore struct {
 33	token ChunkGRC721Token
 34	// Chunk metadata is stored as one encoded string, not a map, to avoid
 35	// dirtying and storing per-field map objects on metadata writes.
 36	metadataByWorldID *btree.Uint32BTree
 37	ownerIndexByWorld *bptree.BPTree
 38}
 39
 40func newNFTStore(token ChunkGRC721Token) *NFTStore {
 41	return &NFTStore{
 42		token:             token,
 43		metadataByWorldID: btree.NewUint32BTree(32),
 44		ownerIndexByWorld: bptree.NewBPTree32(),
 45	}
 46}
 47
 48// ==================== DANGER: MUTABLE MIGRATION CAPABILITIES ====================
 49//
 50// The getters in this section expose mutable token/store internals.
 51// Only authorized migration exporter paths may depend on these capabilities.
 52// Runtime reads and test setup must use copy-returning methods or total helpers.
 53func (s *NFTStore) Token() ChunkGRC721Token {
 54	return s.token
 55}
 56
 57func (s *NFTStore) MetadataByWorldID() *btree.Uint32BTree {
 58	return s.metadataByWorldID
 59}
 60
 61func (s *NFTStore) OwnerIndexByWorld() *bptree.BPTree {
 62	return s.ownerIndexByWorld
 63}
 64
 65// ================= END DANGER: MUTABLE MIGRATION CAPABILITIES ==================
 66
 67func (s *NFTStore) Name() string {
 68	return s.token.Name()
 69}
 70
 71func (s *NFTStore) Symbol() string {
 72	return s.token.Symbol()
 73}
 74
 75func (s *NFTStore) TokenCount() int64 {
 76	return s.token.TokenCount()
 77}
 78
 79func (s *NFTStore) BalanceOf(user address) (int64, error) {
 80	return s.token.BalanceOf(user)
 81}
 82
 83func (s *NFTStore) OwnerOf(tokenID grc721.TokenID) (address, error) {
 84	return s.token.OwnerOf(tokenID)
 85}
 86
 87func (s *NFTStore) OwnerOfSafe(tokenID grc721.TokenID) (address, bool) {
 88	owner, err := s.token.OwnerOf(tokenID)
 89	if err != nil {
 90		return "", false
 91	}
 92	return owner, true
 93}
 94
 95func (s *NFTStore) IsApprovedForAll(owner address, user address) bool {
 96	return s.token.IsApprovedForAll(owner, user)
 97}
 98
 99func (s *NFTStore) GetApproved(tokenID grc721.TokenID) (address, error) {
100	return s.token.GetApproved(tokenID)
101}
102
103func (s *NFTStore) Approve(caller address, user address, tokenID grc721.TokenID) error {
104	return s.token.Approve(caller, user, tokenID)
105}
106
107func (s *NFTStore) SetApprovalForAll(caller address, user address, approved bool) error {
108	return s.token.SetApprovalForAll(caller, user, approved)
109}
110
111func (s *NFTStore) Mint(to address, tokenID grc721.TokenID, worldID uint32, metadata string) error {
112	if err := s.token.Mint(to, tokenID); err != nil {
113		return err
114	}
115	s.addOwnerIndex(worldID, to, tokenID)
116	s.CreateMetadata(worldID, tokenID, metadata)
117	return nil
118}
119
120func (s *NFTStore) Transfer(caller address, from address, to address, tokenID grc721.TokenID, worldID uint32) error {
121	if err := s.token.TransferFrom(caller, from, to, tokenID); err != nil {
122		return err
123	}
124	s.removeOwnerIndex(worldID, from, tokenID)
125	s.addOwnerIndex(worldID, to, tokenID)
126	return nil
127}
128
129func (s *NFTStore) ListOwners(tokenIDs ...grc721.TokenID) []map[string]string {
130	result := []map[string]string{}
131	if len(tokenIDs) == 0 {
132		return result
133	}
134	for _, tokenID := range tokenIDs {
135		owner, err := s.token.OwnerOf(tokenID)
136		if err != nil {
137			continue
138		}
139		result = append(result, map[string]string{"id": string(tokenID), "owner": string(owner)})
140	}
141	return result
142}
143
144func (s *NFTStore) CreateMetadata(worldID uint32, tokenID grc721.TokenID, metadata string) {
145	t := s.getOrCreateMetadataTree(worldID)
146	_, exists := t.Get(string(tokenID))
147	if exists {
148		panic("metadata already exists: " + string(tokenID))
149	}
150	t.Set(string(tokenID), metadata)
151}
152
153func (s *NFTStore) SetChunkMetadata(worldID uint32, tokenID grc721.TokenID, metadata string) {
154	if _, found := s.OwnerOfSafe(tokenID); !found {
155		panic("token not found: " + string(tokenID))
156	}
157	t := s.getOrCreateMetadataTree(worldID)
158	t.Set(string(tokenID), metadata)
159}
160
161func (s *NFTStore) GetChunkMetadata(worldID uint32, tokenID grc721.TokenID) map[string]string {
162	value, exists := s.metadataByWorldID.Get(worldID)
163	if !exists {
164		return nil
165	}
166	t := value.(*btree.StringBTree)
167	result, found := t.Get(string(tokenID))
168	if !found {
169		return nil
170	}
171	return map[string]string{
172		"id":       string(tokenID),
173		"metadata": result.(string),
174	}
175}
176
177func (s *NFTStore) ListChunkMetadataByWorld(worldID uint32, page int, count int) []map[string]string {
178	value, exists := s.metadataByWorldID.Get(worldID)
179	if !exists {
180		return []map[string]string{}
181	}
182	t := value.(*btree.StringBTree)
183	result := []map[string]string{}
184	offset := (page - 1) * count
185	t.IterateByOffset(offset, count, func(key string, value any) bool {
186		result = append(result, map[string]string{
187			"id":       key,
188			"metadata": value.(string),
189		})
190		return false
191	})
192	return result
193}
194
195func (s *NFTStore) WorldMetadataSize(worldID uint32) int {
196	value, exists := s.metadataByWorldID.Get(worldID)
197	if !exists {
198		return 0
199	}
200	return value.(*btree.StringBTree).Size()
201}
202
203func (s *NFTStore) MetadataWorldSize() int {
204	return s.metadataByWorldID.Size()
205}
206
207func (s *NFTStore) ListTokenIDsByOwner(worldID uint32, owner address, page int, count int) []string {
208	tree := s.getOwnerTokenTree(worldID, owner)
209	if tree == nil {
210		return []string{}
211	}
212	result := []string{}
213	offset := (page - 1) * count
214	tree.IterateByOffset(offset, count, func(tokenID string) bool {
215		result = append(result, tokenID)
216		return false
217	})
218	return result
219}
220
221func (s *NFTStore) OwnerTokenSize(worldID uint32, owner address) int {
222	tree := s.getOwnerTokenTree(worldID, owner)
223	if tree == nil {
224		return 0
225	}
226	return tree.Size()
227}
228
229func (s *NFTStore) OwnerWorldSize() int {
230	return s.ownerIndexByWorld.Size()
231}
232
233func (s *NFTStore) getOrCreateMetadataTree(worldID uint32) *btree.StringBTree {
234	value, exists := s.metadataByWorldID.Get(worldID)
235	if !exists {
236		t := btree.NewStringBTree(32)
237		s.metadataByWorldID.Set(worldID, t)
238		return t
239	}
240	return value.(*btree.StringBTree)
241}
242
243func (s *NFTStore) addOwnerIndex(worldID uint32, owner address, tokenID grc721.TokenID) {
244	tree := s.getOrCreateOwnerTokenTree(worldID, owner)
245	tree.Set(string(tokenID))
246}
247
248func (s *NFTStore) removeOwnerIndex(worldID uint32, owner address, tokenID grc721.TokenID) {
249	wKey := formatWorldID(worldID)
250	val, exists := s.ownerIndexByWorld.Get(wKey)
251	if !exists {
252		return
253	}
254	ownerTree := val.(*bptree.BPTree)
255	coordVal, found := ownerTree.Get(owner.String())
256	if !found {
257		return
258	}
259	tokenTree := coordVal.(*btreeset.StringBTreeSet)
260	tokenTree.Remove(string(tokenID))
261}
262
263func (s *NFTStore) getOwnerTokenTree(worldID uint32, owner address) *btreeset.StringBTreeSet {
264	wKey := formatWorldID(worldID)
265	val, exists := s.ownerIndexByWorld.Get(wKey)
266	if !exists {
267		return nil
268	}
269	ownerTree := val.(*bptree.BPTree)
270	coordVal, found := ownerTree.Get(owner.String())
271	if !found {
272		return nil
273	}
274	return coordVal.(*btreeset.StringBTreeSet)
275}
276
277func (s *NFTStore) getOrCreateOwnerTokenTree(worldID uint32, owner address) *btreeset.StringBTreeSet {
278	wKey := formatWorldID(worldID)
279	ownerKey := owner.String()
280	var ownerTree *bptree.BPTree
281	val, exists := s.ownerIndexByWorld.Get(wKey)
282	if !exists {
283		ownerTree = bptree.NewBPTree32()
284		s.ownerIndexByWorld.Set(wKey, ownerTree)
285	} else {
286		ownerTree = val.(*bptree.BPTree)
287	}
288	coordVal, found := ownerTree.Get(ownerKey)
289	if !found {
290		tokenTree := btreeset.NewStringBTreeSet(32)
291		ownerTree.Set(ownerKey, tokenTree)
292		return tokenTree
293	}
294	return coordVal.(*btreeset.StringBTreeSet)
295}