Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}