Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}