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}