acr.gno
8.08 Kb ยท 377 lines
1package acr
2
3import (
4 "chain"
5 "chain/runtime"
6 "strconv"
7
8 "gno.land/p/demo/tokens/grc20"
9 "gno.land/p/nt/avl"
10 "gno.land/p/nt/ufmt"
11)
12
13const (
14 MintEvent = "Mint"
15)
16
17var (
18 token *grc20.Token
19 ledger *grc20.PrivateLedger
20
21 // Index for top users by balance: "paddedBalance" -> map[address]bool
22 balanceIndex = avl.NewTree()
23 // Index for top users by total minted amount: "paddedMint" -> map[address]bool
24 mintIndex = avl.NewTree()
25 // Track total minted amount per user
26 userMintTotal = avl.NewTree() // address -> int64
27 // Track processed mint requests for deduplication
28 processedRequests = avl.NewTree() // requestID -> bool
29)
30
31func init() {
32 token, ledger = grc20.NewToken("Akkadia Community Rune", "ACR", 6)
33}
34
35// ==================== GRC-20 Standard ====================
36
37func GetName() string {
38 return token.GetName()
39}
40
41func GetSymbol() string {
42 return token.GetSymbol()
43}
44
45func GetDecimals() int {
46 return token.GetDecimals()
47}
48
49func TotalSupply() int64 {
50 return token.TotalSupply()
51}
52
53func BalanceOf(account address) int64 {
54 return token.BalanceOf(account)
55}
56
57func Transfer(cur realm, to address, amount int64) {
58 caller := runtime.PreviousRealm().Address()
59 oldFromBalance := token.BalanceOf(caller)
60 oldToBalance := token.BalanceOf(to)
61
62 teller := token.CallerTeller()
63 err := teller.Transfer(to, amount)
64 if err != nil {
65 panic("transfer failed: " + err.Error())
66 }
67
68 // Update balance index
69 updateBalanceIndex(caller, oldFromBalance, token.BalanceOf(caller))
70 updateBalanceIndex(to, oldToBalance, token.BalanceOf(to))
71}
72
73func Allowance(owner, spender address) int64 {
74 return token.Allowance(owner, spender)
75}
76
77func Approve(cur realm, spender address, amount int64) {
78 teller := token.CallerTeller()
79 err := teller.Approve(spender, amount)
80 if err != nil {
81 panic("approve failed: " + err.Error())
82 }
83}
84
85func TransferFrom(cur realm, from, to address, amount int64) {
86 oldFromBalance := token.BalanceOf(from)
87 oldToBalance := token.BalanceOf(to)
88
89 teller := token.CallerTeller()
90 err := teller.TransferFrom(from, to, amount)
91 if err != nil {
92 panic("transferFrom failed: " + err.Error())
93 }
94
95 // Update balance index
96 updateBalanceIndex(from, oldFromBalance, token.BalanceOf(from))
97 updateBalanceIndex(to, oldToBalance, token.BalanceOf(to))
98}
99
100// ==================== Admin Functions ====================
101
102// Mint mints ACR tokens to a user
103func Mint(cur realm, requestID string, to address, amount int64) {
104 caller := runtime.PreviousRealm().Address()
105 assertIsAdminOrOperator(caller)
106
107 if requestID == "" {
108 panic("requestID is required")
109 }
110 if processedRequests.Has(requestID) {
111 panic("requestID already processed")
112 }
113 if amount <= 0 {
114 panic("amount must be positive")
115 }
116
117 oldBalance := token.BalanceOf(to)
118
119 err := ledger.Mint(to, amount)
120 if err != nil {
121 panic("mint failed: " + err.Error())
122 }
123
124 // Mark request as processed
125 processedRequests.Set(requestID, true)
126
127 // Update balance index
128 updateBalanceIndex(to, oldBalance, token.BalanceOf(to))
129
130 // Update mint index
131 updateMintIndex(to, amount)
132
133 chain.Emit(
134 MintEvent,
135 "requestID", requestID,
136 "to", to.String(),
137 "amount", strconv.FormatInt(amount, 10),
138 )
139}
140
141// IsRequestProcessed checks if a requestID has been processed
142func IsRequestProcessed(requestID string) bool {
143 return processedRequests.Has(requestID)
144}
145
146// ListRequestProcessed checks multiple requestIDs and returns their processed status
147func ListRequestProcessed(requestIDs ...string) map[string]bool {
148 if len(requestIDs) > listLimit {
149 panic("requestIDs exceeds listLimit")
150 }
151
152 result := make(map[string]bool)
153 for _, id := range requestIDs {
154 result[id] = processedRequests.Has(id)
155 }
156 return result
157}
158
159// Burn burns ACR tokens from caller
160func Burn(cur realm, amount int64) {
161 caller := runtime.PreviousRealm().Address()
162
163 if amount <= 0 {
164 panic("amount must be positive")
165 }
166
167 oldBalance := token.BalanceOf(caller)
168
169 err := ledger.Burn(caller, amount)
170 if err != nil {
171 panic("burn failed: " + err.Error())
172 }
173
174 // Update balance index
175 updateBalanceIndex(caller, oldBalance, token.BalanceOf(caller))
176}
177
178// ==================== Balance Index ====================
179
180func balanceToKey(balance int64) string {
181 // Pad to 20 digits for proper AVL tree sorting
182 return ufmt.Sprintf("%020d", balance)
183}
184
185func addToBalanceIndex(addr address, balance int64) {
186 key := balanceToKey(balance)
187 value, found := balanceIndex.Get(key)
188
189 var users map[address]bool
190 if !found {
191 users = map[address]bool{}
192 balanceIndex.Set(key, users)
193 } else {
194 users = value.(map[address]bool)
195 }
196 users[addr] = true
197}
198
199func removeFromBalanceIndex(addr address, balance int64) {
200 key := balanceToKey(balance)
201 value, found := balanceIndex.Get(key)
202 if !found {
203 return
204 }
205
206 users := value.(map[address]bool)
207 delete(users, addr)
208
209 if len(users) == 0 {
210 balanceIndex.Remove(key)
211 }
212}
213
214func updateBalanceIndex(addr address, oldBalance, newBalance int64) {
215 // Remove from old balance
216 if oldBalance > 0 {
217 removeFromBalanceIndex(addr, oldBalance)
218 }
219
220 // Add to new balance
221 if newBalance > 0 {
222 addToBalanceIndex(addr, newBalance)
223 }
224}
225
226// ListTopUsersByBalance returns top users sorted by balance (descending) with pagination
227func ListTopUsersByBalance(page, count int) []map[string]string {
228 if page < 1 {
229 panic("page must be at least 1")
230 }
231 if count < 1 {
232 panic("count must be at least 1")
233 }
234 if count > listLimit {
235 panic("count exceeds listLimit")
236 }
237
238 var result []map[string]string
239 offset := (page - 1) * count
240 skipped := 0
241 collected := 0
242
243 // ReverseIterate gives us high-to-low balance order
244 balanceIndex.ReverseIterate("", "", func(key string, value any) bool {
245 if collected >= count {
246 return true // stop iteration
247 }
248
249 users := value.(map[address]bool)
250 for addr := range users {
251 // Skip until we reach offset
252 if skipped < offset {
253 skipped++
254 continue
255 }
256
257 if collected >= count {
258 return true
259 }
260
261 balance := token.BalanceOf(addr)
262 result = append(result, map[string]string{
263 "user": addr.String(),
264 "balance": ufmt.Sprintf("%d", balance),
265 })
266 collected++
267 }
268
269 return false
270 })
271
272 return result
273}
274
275// ==================== Mint Index ====================
276
277func MintedOf(account address) int64 {
278 value, found := userMintTotal.Get(account.String())
279 if !found {
280 return 0
281 }
282 return value.(int64)
283}
284
285func addToMintIndex(addr address, mintTotal int64) {
286 key := balanceToKey(mintTotal)
287 value, found := mintIndex.Get(key)
288
289 var users map[address]bool
290 if !found {
291 users = map[address]bool{}
292 mintIndex.Set(key, users)
293 } else {
294 users = value.(map[address]bool)
295 }
296 users[addr] = true
297}
298
299func removeFromMintIndex(addr address, mintTotal int64) {
300 key := balanceToKey(mintTotal)
301 value, found := mintIndex.Get(key)
302 if !found {
303 return
304 }
305
306 users := value.(map[address]bool)
307 delete(users, addr)
308
309 if len(users) == 0 {
310 mintIndex.Remove(key)
311 }
312}
313
314func updateMintIndex(addr address, amount int64) {
315 oldMintTotal := MintedOf(addr)
316 newMintTotal := oldMintTotal + amount
317
318 // Update user mint total
319 userMintTotal.Set(addr.String(), newMintTotal)
320
321 // Remove from old mint total
322 if oldMintTotal > 0 {
323 removeFromMintIndex(addr, oldMintTotal)
324 }
325
326 // Add to new mint total
327 addToMintIndex(addr, newMintTotal)
328}
329
330// ListTopUsersByMinting returns top users sorted by total minted amount (descending) with pagination
331func ListTopUsersByMinting(page, count int) []map[string]string {
332 if page < 1 {
333 panic("page must be at least 1")
334 }
335 if count < 1 {
336 panic("count must be at least 1")
337 }
338 if count > listLimit {
339 panic("count exceeds listLimit")
340 }
341
342 var result []map[string]string
343 offset := (page - 1) * count
344 skipped := 0
345 collected := 0
346
347 // ReverseIterate gives us high-to-low mint order
348 mintIndex.ReverseIterate("", "", func(key string, value any) bool {
349 if collected >= count {
350 return true // stop iteration
351 }
352
353 users := value.(map[address]bool)
354 for addr := range users {
355 // Skip until we reach offset
356 if skipped < offset {
357 skipped++
358 continue
359 }
360
361 if collected >= count {
362 return true
363 }
364
365 minted := MintedOf(addr)
366 result = append(result, map[string]string{
367 "user": addr.String(),
368 "minted": ufmt.Sprintf("%d", minted),
369 })
370 collected++
371 }
372
373 return false
374 })
375
376 return result
377}