minted_block_store.gno
9.91 Kb · 371 lines
1package block
2
3import (
4 "strconv"
5
6 "gno.land/p/akkadia/v0/ds/btree"
7 "gno.land/p/akkadia/v0/ds/btreeset"
8 "gno.land/p/akkadia/v0/grc1155"
9)
10
11var mintedBlockStore = newMintedBlockStore()
12
13// MintedBlockStore owns token state, lifetime supply counters, inventory indexes,
14// and canonical mint allowlist membership.
15//
16// Data shape:
17// - token: GRC-1155 token state
18// - supply: blockID(uint32) -> lifetime supply(int64)
19// - userTokenIndex: user(address string) -> *Uint32BTreeSet(blockID)
20// - mintAllowlist: blockID(uint32) -> map[address string]bool
21//
22// Non-responsibilities:
23// - parse user-facing CSV input or validate external address string shape
24// - perform auth, freeze, migration, payment, or event handling
25type MintedBlockStore struct {
26 token BlockGRC1155Token
27 // supply tracks cumulative minted/reserved supply and is not decremented on burn.
28 // It enforces lifetime maxSupply and mintable block delete policy.
29 supply *btree.Uint32BTree
30 userTokenIndex *btree.StringBTree
31 mintAllowlist *btree.Uint32BTree
32}
33
34func newMintedBlockStore() *MintedBlockStore {
35 return &MintedBlockStore{
36 token: grc1155.NewBasicGRC1155Token(""),
37 supply: btree.NewUint32BTree(32),
38 userTokenIndex: btree.NewStringBTree(32),
39 mintAllowlist: btree.NewUint32BTree(32),
40 }
41}
42
43// ==================== DANGER: MUTABLE MIGRATION CAPABILITIES ====================
44//
45// The getters in this section expose mutable token/store internals.
46// Only authorized migration exporter paths may depend on these capabilities.
47// Runtime reads and test setup must use copy-returning methods or total helpers.
48func (s *MintedBlockStore) Token() BlockGRC1155Token {
49 return s.token
50}
51
52func (s *MintedBlockStore) Supply() *btree.Uint32BTree {
53 return s.supply
54}
55
56func (s *MintedBlockStore) UserTokenIndex() *btree.StringBTree {
57 return s.userTokenIndex
58}
59
60func (s *MintedBlockStore) MintAllowlist() *btree.Uint32BTree {
61 return s.mintAllowlist
62}
63
64// ================= END DANGER: MUTABLE MIGRATION CAPABILITIES ==================
65
66func (s *MintedBlockStore) BalanceOf(user address, tokenID grc1155.TokenID) int64 {
67 balance, err := s.token.BalanceOf(user, tokenID)
68 if err != nil {
69 panic("balanceOf failed: " + err.Error())
70 }
71 return balance
72}
73
74func (s *MintedBlockStore) BalanceOfBatch(ul []address, tokenIDs []grc1155.TokenID) []int64 {
75 balanceBatch, err := s.token.BalanceOfBatch(ul, tokenIDs)
76 if err != nil {
77 panic("balanceOfBatch failed: " + err.Error())
78 }
79 return balanceBatch
80}
81
82func (s *MintedBlockStore) BalanceOfSafe(user address, tokenID grc1155.TokenID) (int64, bool) {
83 balance, err := s.token.BalanceOf(user, tokenID)
84 if err != nil {
85 return 0, false
86 }
87 return balance, true
88}
89
90func (s *MintedBlockStore) IsApprovedForAll(owner, operator address) bool {
91 return s.token.IsApprovedForAll(owner, operator)
92}
93
94func (s *MintedBlockStore) SetApprovalForAll(caller, operator address, approved bool) {
95 err := s.token.SetApprovalForAll(caller, operator, approved)
96 if err != nil {
97 panic("setApprovalForAll failed: " + err.Error())
98 }
99}
100
101func (s *MintedBlockStore) Transfer(caller, from, to address, tokenID grc1155.TokenID, amount int64) {
102 err := s.token.SafeTransferFrom(caller, from, to, tokenID, amount)
103 if err != nil {
104 panic("transferFrom failed: " + err.Error())
105 }
106
107 blockID := tokenIDToBlockID(tokenID)
108 s.addUserToken(to, blockID)
109 s.removeUserToken(from, blockID)
110}
111
112func (s *MintedBlockStore) BatchTransfer(caller, from, to address, tokenIDs []grc1155.TokenID, amounts []int64) {
113 err := s.token.SafeBatchTransferFrom(caller, from, to, tokenIDs, amounts)
114 if err != nil {
115 panic("batchTransferFrom failed: " + err.Error())
116 }
117
118 blockIDs := tokenIDsToBlockIDs(tokenIDs)
119 s.addUserTokens(to, blockIDs)
120 s.removeUserTokens(from, blockIDs)
121}
122
123func (s *MintedBlockStore) Mint(caller, to address, tokenID grc1155.TokenID, amount int64, maxSupply int64) {
124 blockID := tokenIDToBlockID(tokenID)
125
126 if s.exceedsSupply(blockID, amount, maxSupply) {
127 panic("supply exceeded")
128 }
129
130 err := s.token.SafeMint(caller, to, tokenID, amount)
131 if err != nil {
132 panic("mint failed: " + err.Error())
133 }
134
135 nextSupply := s.SupplyOf(blockID) + amount
136 s.supply.Set(blockID, nextSupply)
137 s.addUserToken(to, blockID)
138}
139
140func (s *MintedBlockStore) Burn(caller, from address, tokenID grc1155.TokenID, amount int64) {
141 err := s.token.Burn(caller, from, tokenID, amount)
142 if err != nil {
143 panic("burn failed: " + err.Error())
144 }
145
146 s.removeUserToken(from, tokenIDToBlockID(tokenID))
147}
148
149func (s *MintedBlockStore) BatchBurn(caller, from address, tokenIDs []grc1155.TokenID, amounts []int64) {
150 err := s.token.BatchBurn(caller, from, tokenIDs, amounts)
151 if err != nil {
152 panic("batchBurn failed: " + err.Error())
153 }
154
155 s.removeUserTokens(from, tokenIDsToBlockIDs(tokenIDs))
156}
157
158func (s *MintedBlockStore) ListSupplies(tokenIDs ...grc1155.TokenID) []map[string]string {
159 result := []map[string]string{}
160 if len(tokenIDs) == 0 {
161 return result
162 }
163
164 for _, tokenID := range tokenIDs {
165 blockID := tokenIDToBlockID(tokenID)
166 currentSupply, found := s.supply.Get(blockID)
167 if !found {
168 continue
169 }
170
171 result = append(result, map[string]string{
172 "id": string(tokenID),
173 "supply": strconv.FormatInt(currentSupply.(int64), 10),
174 })
175 }
176 return result
177}
178
179func (s *MintedBlockStore) SupplyTotal() int {
180 return s.supply.Size()
181}
182
183func (s *MintedBlockStore) SupplyOf(blockID uint32) int64 {
184 currentSupply, found := s.supply.Get(blockID)
185 if !found {
186 return 0
187 }
188 return currentSupply.(int64)
189}
190
191func (s *MintedBlockStore) exceedsSupply(blockID uint32, amount int64, maxSupply int64) bool {
192 currentSupply := s.SupplyOf(blockID)
193 if currentSupply > maxSupply {
194 return true
195 }
196 return amount > maxSupply-currentSupply
197}
198
199func (s *MintedBlockStore) addUserToken(user address, blockID uint32) {
200 tokens := s.getOrCreateUserTokenSet(user)
201 tokens.Set(blockID)
202}
203
204func (s *MintedBlockStore) addUserTokens(user address, blockIDs []uint32) {
205 tokens := s.getOrCreateUserTokenSet(user)
206 for _, blockID := range blockIDs {
207 tokens.Set(blockID)
208 }
209}
210
211// removeUserToken only prunes the inventory index after balance reaches zero.
212// Supply is a lifetime minted counter, so burn does not decrement it by policy.
213func (s *MintedBlockStore) removeUserToken(user address, blockID uint32) {
214 balance := s.BalanceOf(user, blockIDToTokenID(blockID))
215 if balance != 0 {
216 return
217 }
218
219 userStr := user.String()
220 tokens, found := s.userTokenIndex.Get(userStr)
221 if !found {
222 return
223 }
224
225 tokensSet := tokens.(*btreeset.Uint32BTreeSet)
226 tokensSet.Remove(blockID)
227 if tokensSet.Size() == 0 {
228 s.userTokenIndex.Remove(userStr)
229 }
230}
231
232// removeUserTokens only prunes the inventory index after each balance reaches zero.
233// Supply is a lifetime minted counter, so burn does not decrement it by policy.
234func (s *MintedBlockStore) removeUserTokens(user address, blockIDs []uint32) {
235 userStr := user.String()
236 tokens, found := s.userTokenIndex.Get(userStr)
237 if !found {
238 return
239 }
240
241 tokensSet := tokens.(*btreeset.Uint32BTreeSet)
242 for _, blockID := range blockIDs {
243 balance := s.BalanceOf(user, blockIDToTokenID(blockID))
244 if balance != 0 {
245 continue
246 }
247 tokensSet.Remove(blockID)
248 }
249 if tokensSet.Size() == 0 {
250 s.userTokenIndex.Remove(userStr)
251 }
252}
253
254func (s *MintedBlockStore) getUserTokens(user address) []uint32 {
255 userStr := user.String()
256 tokens, found := s.userTokenIndex.Get(userStr)
257 if !found {
258 return []uint32{}
259 }
260
261 tokensSet := tokens.(*btreeset.Uint32BTreeSet)
262 result := []uint32{}
263 tokensSet.Iterate(nil, nil, func(blockID uint32) bool {
264 result = append(result, blockID)
265 return false
266 })
267 return result
268}
269
270func (s *MintedBlockStore) GetInventory(user address) []map[string]string {
271 result := []map[string]string{}
272 blockIDs := s.getUserTokens(user)
273 for _, blockID := range blockIDs {
274 blockIDStr := blockIDToString(blockID)
275 balance := s.BalanceOf(user, blockIDToTokenID(blockID))
276 if balance > 0 {
277 result = append(result, map[string]string{
278 "id": blockIDStr,
279 "balance": strconv.FormatInt(balance, 10),
280 })
281 }
282 }
283 return result
284}
285
286func (s *MintedBlockStore) UserInventoryTotal() int {
287 return s.userTokenIndex.Size()
288}
289
290func (s *MintedBlockStore) SetMintAllowlist(blockID uint32, minters []address) {
291 var mintersMap map[string]bool
292 val, found := s.mintAllowlist.Get(blockID)
293 if found {
294 mintersMap = val.(map[string]bool)
295 } else {
296 mintersMap = make(map[string]bool)
297 }
298
299 for _, minter := range minters {
300 mintersMap[minter.String()] = true
301 }
302
303 if !found {
304 s.mintAllowlist.Set(blockID, mintersMap)
305 }
306}
307
308func (s *MintedBlockStore) RemoveMintAllowlist(blockID uint32, minters []address) {
309 blockIDStr := blockIDToString(blockID)
310
311 val, found := s.mintAllowlist.Get(blockID)
312 if !found {
313 panic("mint allowlist not found: " + blockIDStr)
314 }
315
316 mintersMap := val.(map[string]bool)
317 for _, minter := range minters {
318 addrStr := minter.String()
319 if !mintersMap[addrStr] {
320 panic("minter not found in allowlist: " + addrStr)
321 }
322 delete(mintersMap, addrStr)
323 }
324
325 if len(mintersMap) == 0 {
326 s.mintAllowlist.Remove(blockID)
327 }
328}
329
330func (s *MintedBlockStore) GetMintAllowlist(blockID uint32) []string {
331 val, found := s.mintAllowlist.Get(blockID)
332 if !found {
333 return []string{}
334 }
335
336 mintersMap := val.(map[string]bool)
337 result := make([]string, 0, len(mintersMap))
338 for addr := range mintersMap {
339 result = append(result, addr)
340 }
341 return result
342}
343
344func (s *MintedBlockStore) CanUserMint(blockID uint32, caller address) bool {
345 val, found := s.mintAllowlist.Get(blockID)
346 if !found {
347 return true
348 }
349 mintersMap := val.(map[string]bool)
350 return mintersMap[caller.String()]
351}
352
353func (s *MintedBlockStore) MintAllowlistTotal() int {
354 return s.mintAllowlist.Size()
355}
356
357func (s *MintedBlockStore) ClearMintAllowlist(blockID uint32) {
358 s.mintAllowlist.Remove(blockID)
359}
360
361func (s *MintedBlockStore) getOrCreateUserTokenSet(user address) *btreeset.Uint32BTreeSet {
362 userStr := user.String()
363 tokens, found := s.userTokenIndex.Get(userStr)
364 if found {
365 return tokens.(*btreeset.Uint32BTreeSet)
366 }
367
368 tokensSet := btreeset.NewUint32BTreeSet(32)
369 s.userTokenIndex.Set(userStr, tokensSet)
370 return tokensSet
371}