Search Apps Documentation Source Content File Folder Download Copy Actions Download

acr_store.gno

10.28 Kb · 416 lines
  1package acr
  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/demo/tokens/grc20"
  9)
 10
 11const (
 12	maxInt64 = int64(9223372036854775807)
 13
 14	maxHofCategoryCount = 1024
 15)
 16
 17// ACRStore owns ACR token state, ranking indexes, request IDs, and HOF data.
 18//
 19// Data shape:
 20// - token: GRC-20 token state
 21// - ledger: private GRC-20 ledger capability
 22// - balanceIndex: balance(int64) -> *StringBTreeSet(account address string)
 23// - mintIndex: minted(int64) -> *StringBTreeSet(account address string)
 24// - userMintTotal: account(address string) -> minted(int64)
 25// - processedRequests: requestID(string) set
 26// - hofCategories: category(string) -> CSV rows(string)
 27type ACRStore struct {
 28	token             *grc20.Token
 29	ledger            *grc20.PrivateLedger
 30	balanceIndex      *btree.Int64BTree
 31	mintIndex         *btree.Int64BTree
 32	userMintTotal     *btree.StringBTree
 33	processedRequests *btreeset.StringBTreeSet
 34	hofCategories     *btree.StringBTree
 35}
 36
 37func newACRStore(cur realm) *ACRStore {
 38	token, ledger := grc20.NewToken(0, cur, "Akkadia Community Rune", "ACR", 6)
 39	return &ACRStore{
 40		token:             token,
 41		ledger:            ledger,
 42		balanceIndex:      btree.NewInt64BTree(32),
 43		mintIndex:         btree.NewInt64BTree(32),
 44		userMintTotal:     btree.NewStringBTree(32),
 45		processedRequests: btreeset.NewStringBTreeSet(32),
 46		hofCategories:     btree.NewStringBTree(32),
 47	}
 48}
 49
 50// ==================== DANGER: MUTABLE MIGRATION CAPABILITIES ====================
 51//
 52// The getters in this section expose mutable token/store internals.
 53// Only authorized migration exporter paths may depend on these capabilities.
 54// Runtime reads and test setup must use copy-returning methods or total helpers.
 55func (s *ACRStore) Token() *grc20.Token {
 56	return s.token
 57}
 58
 59func (s *ACRStore) Ledger() *grc20.PrivateLedger {
 60	return s.ledger
 61}
 62
 63func (s *ACRStore) BalanceIndex() *btree.Int64BTree {
 64	return s.balanceIndex
 65}
 66
 67func (s *ACRStore) MintIndex() *btree.Int64BTree {
 68	return s.mintIndex
 69}
 70
 71func (s *ACRStore) UserMintTotal() *btree.StringBTree {
 72	return s.userMintTotal
 73}
 74
 75func (s *ACRStore) ProcessedRequests() *btreeset.StringBTreeSet {
 76	return s.processedRequests
 77}
 78
 79func (s *ACRStore) HofCategories() *btree.StringBTree {
 80	return s.hofCategories
 81}
 82
 83// ================= END DANGER: MUTABLE MIGRATION CAPABILITIES ==================
 84
 85func (s *ACRStore) GetName() string {
 86	return s.token.GetName()
 87}
 88
 89func (s *ACRStore) GetSymbol() string {
 90	return s.token.GetSymbol()
 91}
 92
 93func (s *ACRStore) GetDecimals() int {
 94	return s.token.GetDecimals()
 95}
 96
 97func (s *ACRStore) KnownAccounts() int {
 98	return s.token.KnownAccounts()
 99}
100
101func (s *ACRStore) TotalSupply() int64 {
102	return s.token.TotalSupply()
103}
104
105func (s *ACRStore) BalanceOf(account address) int64 {
106	return s.token.BalanceOf(account)
107}
108
109func (s *ACRStore) Allowance(owner, spender address) int64 {
110	return s.token.Allowance(owner, spender)
111}
112
113func (s *ACRStore) Transfer(from, to address, amount int64) {
114	oldFromBalance := s.BalanceOf(from)
115	oldToBalance := s.BalanceOf(to)
116
117	err := s.ledger.Transfer(from, to, amount)
118	if err != nil {
119		panic("transfer failed: " + err.Error())
120	}
121
122	s.updateBalanceIndex(from, oldFromBalance, s.BalanceOf(from))
123	s.updateBalanceIndex(to, oldToBalance, s.BalanceOf(to))
124}
125
126func (s *ACRStore) Approve(owner, spender address, amount int64) {
127	currentAllowance := s.Allowance(owner, spender)
128	if amount != 0 && currentAllowance != 0 {
129		panic("approve failed: must reset allowance to 0 before setting a new non-zero allowance")
130	}
131
132	err := s.ledger.Approve(owner, spender, amount)
133	if err != nil {
134		panic("approve failed: " + err.Error())
135	}
136}
137
138func (s *ACRStore) TransferFrom(from, spender, to address, amount int64) {
139	oldFromBalance := s.BalanceOf(from)
140	oldToBalance := s.BalanceOf(to)
141
142	err := s.ledger.TransferFrom(from, spender, to, amount)
143	if err != nil {
144		panic("transferFrom failed: " + err.Error())
145	}
146
147	s.updateBalanceIndex(from, oldFromBalance, s.BalanceOf(from))
148	s.updateBalanceIndex(to, oldToBalance, s.BalanceOf(to))
149}
150
151func (s *ACRStore) Mint(requestID string, to address, amount int64) {
152	if requestID == "" {
153		panic("requestID is required")
154	}
155	if s.processedRequests.Has(requestID) {
156		panic("requestID already processed")
157	}
158	if amount <= 0 {
159		panic("amount must be positive")
160	}
161
162	oldBalance := s.BalanceOf(to)
163	oldMintTotal := s.MintedOf(to)
164	newMintTotal := calculateMintTotal(oldMintTotal, amount)
165
166	err := s.ledger.Mint(to, amount)
167	if err != nil {
168		panic("mint failed: " + err.Error())
169	}
170
171	s.processedRequests.Set(requestID)
172	s.updateBalanceIndex(to, oldBalance, s.BalanceOf(to))
173	s.updateMintIndex(to, oldMintTotal, newMintTotal)
174}
175
176func (s *ACRStore) Burn(caller address, amount int64) {
177	if amount <= 0 {
178		panic("amount must be positive")
179	}
180
181	oldBalance := s.BalanceOf(caller)
182
183	err := s.ledger.Burn(caller, amount)
184	if err != nil {
185		panic("burn failed: " + err.Error())
186	}
187
188	s.updateBalanceIndex(caller, oldBalance, s.BalanceOf(caller))
189}
190
191func (s *ACRStore) IsRequestProcessed(requestID string) bool {
192	return s.processedRequests.Has(requestID)
193}
194
195func (s *ACRStore) ListRequestProcessed(requestIDs ...string) map[string]bool {
196	result := make(map[string]bool)
197	for _, id := range requestIDs {
198		result[id] = s.processedRequests.Has(id)
199	}
200	return result
201}
202
203func (s *ACRStore) MintedOf(account address) int64 {
204	value, found := s.userMintTotal.Get(account.String())
205	if !found {
206		return 0
207	}
208	return value.(int64)
209}
210
211func (s *ACRStore) ListTopUsersByBalance(page int, count int) []map[string]string {
212	result := []map[string]string{}
213	skipped := 0
214	offset := pageOffset(page, count)
215	s.balanceIndex.ReverseIterate(nil, nil, func(balance int64, value any) bool {
216		if len(result) >= count {
217			return true
218		}
219
220		users := value.(*btreeset.StringBTreeSet)
221		users.Iterate(nil, nil, func(addr string) bool {
222			if skipped < offset {
223				skipped++
224				return false
225			}
226			result = append(result, map[string]string{
227				"user":    addr,
228				"balance": strconv.FormatInt(balance, 10),
229			})
230			return len(result) >= count
231		})
232
233		return false
234	})
235	return result
236}
237
238func (s *ACRStore) ListTopUsersByMinting(page int, count int) []map[string]string {
239	result := []map[string]string{}
240	skipped := 0
241	offset := pageOffset(page, count)
242	s.mintIndex.ReverseIterate(nil, nil, func(minted int64, value any) bool {
243		if len(result) >= count {
244			return true
245		}
246
247		users := value.(*btreeset.StringBTreeSet)
248		users.Iterate(nil, nil, func(addr string) bool {
249			if skipped < offset {
250				skipped++
251				return false
252			}
253			result = append(result, map[string]string{
254				"user":   addr,
255				"minted": strconv.FormatInt(minted, 10),
256			})
257			return len(result) >= count
258		})
259
260		return false
261	})
262	return result
263}
264
265func (s *ACRStore) CreateHofCategory(name string) {
266	if s.hofCategories.Size() >= maxHofCategoryCount {
267		panic("hof categories exceeds maxHofCategoryCount")
268	}
269	if s.hofCategories.Has(name) {
270		panic("category already exists: " + name)
271	}
272	s.hofCategories.Set(name, "")
273}
274
275func (s *ACRStore) RemoveHofCategory(name string) {
276	if !s.hofCategories.Has(name) {
277		panic("category not found: " + name)
278	}
279	s.hofCategories.Remove(name)
280}
281
282func (s *ACRStore) SetHofEntries(category string, csvRows string) {
283	if !s.hofCategories.Has(category) {
284		panic("category not found: " + category)
285	}
286	s.hofCategories.Set(category, csvRows)
287}
288
289func (s *ACRStore) ListHofCategories(page int, count int) []string {
290	categories := []string{}
291	s.hofCategories.IterateByOffset(pageOffset(page, count), count, func(name string, _ any) bool {
292		categories = append(categories, name)
293		return false
294	})
295	return categories
296}
297
298func (s *ACRStore) GetHofEntries(category string) string {
299	csv, exists := s.hofCategories.Get(category)
300	if !exists {
301		panic("category not found: " + category)
302	}
303	return csv.(string)
304}
305
306func (s *ACRStore) BalanceIndexCount() int {
307	return s.balanceIndex.Size()
308}
309
310func (s *ACRStore) MintIndexCount() int {
311	return s.mintIndex.Size()
312}
313
314func (s *ACRStore) UserMintTotalCount() int {
315	return s.userMintTotal.Size()
316}
317
318func (s *ACRStore) ProcessedRequestCount() int {
319	return s.processedRequests.Size()
320}
321
322func (s *ACRStore) HofCategoryCount() int {
323	return s.hofCategories.Size()
324}
325
326func (s *ACRStore) BalanceRankingCount() int {
327	total := 0
328	s.balanceIndex.Iterate(nil, nil, func(_ int64, value any) bool {
329		total += value.(*btreeset.StringBTreeSet).Size()
330		return false
331	})
332	return total
333}
334
335func (s *ACRStore) MintingRankingCount() int {
336	total := 0
337	s.mintIndex.Iterate(nil, nil, func(_ int64, value any) bool {
338		total += value.(*btreeset.StringBTreeSet).Size()
339		return false
340	})
341	return total
342}
343
344func (s *ACRStore) addToBalanceIndex(addr address, balance int64) {
345	value, found := s.balanceIndex.Get(balance)
346	var users *btreeset.StringBTreeSet
347	if !found {
348		users = btreeset.NewStringBTreeSet(32)
349		s.balanceIndex.Set(balance, users)
350	} else {
351		users = value.(*btreeset.StringBTreeSet)
352	}
353	users.Set(addr.String())
354}
355
356func (s *ACRStore) removeFromBalanceIndex(addr address, balance int64) {
357	value, found := s.balanceIndex.Get(balance)
358	if !found {
359		return
360	}
361
362	users := value.(*btreeset.StringBTreeSet)
363	users.Remove(addr.String())
364	if users.Size() == 0 {
365		s.balanceIndex.Remove(balance)
366	}
367}
368
369func (s *ACRStore) updateBalanceIndex(addr address, oldBalance, newBalance int64) {
370	if oldBalance > 0 {
371		s.removeFromBalanceIndex(addr, oldBalance)
372	}
373	if newBalance > 0 {
374		s.addToBalanceIndex(addr, newBalance)
375	}
376}
377
378func (s *ACRStore) addToMintIndex(addr address, mintTotal int64) {
379	value, found := s.mintIndex.Get(mintTotal)
380	var users *btreeset.StringBTreeSet
381	if !found {
382		users = btreeset.NewStringBTreeSet(32)
383		s.mintIndex.Set(mintTotal, users)
384	} else {
385		users = value.(*btreeset.StringBTreeSet)
386	}
387	users.Set(addr.String())
388}
389
390func (s *ACRStore) removeFromMintIndex(addr address, mintTotal int64) {
391	value, found := s.mintIndex.Get(mintTotal)
392	if !found {
393		return
394	}
395
396	users := value.(*btreeset.StringBTreeSet)
397	users.Remove(addr.String())
398	if users.Size() == 0 {
399		s.mintIndex.Remove(mintTotal)
400	}
401}
402
403func (s *ACRStore) updateMintIndex(addr address, oldMintTotal, newMintTotal int64) {
404	s.userMintTotal.Set(addr.String(), newMintTotal)
405	if oldMintTotal > 0 {
406		s.removeFromMintIndex(addr, oldMintTotal)
407	}
408	s.addToMintIndex(addr, newMintTotal)
409}
410
411func calculateMintTotal(oldMintTotal, amount int64) int64 {
412	if oldMintTotal < 0 || amount > maxInt64-oldMintTotal {
413		panic("mint total overflow")
414	}
415	return oldMintTotal + amount
416}