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}