coins.gno
5.94 Kb · 205 lines
1// GnoMoji — Unified Coin Realm
2// gno.land/r/g138gdmwj6heduu3st796lw6qqdwylgaxf7ty5gz/coins
3//
4// Single realm managing all GnoMoji GRC20-style token balances.
5// Only the authorized minter address can call Mint or Burn.
6// Players can call Transfer to move coins to other addresses.
7//
8// Balance storage: AVL tree with key "coinId:addr" → amount string.
9// Total supply: AVL tree with key "coinId" → total minted string.
10
11package coins
12
13import (
14 "strconv"
15 "strings"
16
17 "gno.land/p/nt/avl/v0"
18 "gno.land/p/nt/ufmt/v0"
19)
20
21var (
22 minterAddr address // set to minter4 realm address post-deploy via SetMinter
23 adminAddr address // set at init to deployer
24 balances avl.Tree // key: "coinId:addr" → amount string
25 supplies avl.Tree // key: "coinId" → total supply string
26)
27
28func init(cur realm) {
29 adminAddr = cur.Previous().Address()
30}
31
32// SetMinter sets the authorized minter address.
33// Called once after the minter realm is deployed, by the admin.
34func SetMinter(cur realm, newMinter string) {
35 if cur.Previous().Address() != adminAddr {
36 panic("coins: only admin can set minter")
37 }
38 minterAddr = address(newMinter)
39}
40
41func isMinter(cur realm) bool {
42 return cur.Previous().Address() == minterAddr
43}
44
45func isAdmin(cur realm) bool {
46 return cur.Previous().Address() == adminAddr
47}
48
49// ── Core operations ──────────────────────────────────────────────────────────
50
51// Mint creates new tokens of coinId and credits them to toAddr.
52// Only the authorized minter or admin can call this.
53func Mint(cur realm, coinId string, toAddr string, amountStr string) {
54 if !isMinter(cur) && !isAdmin(cur) {
55 panic("coins: unauthorized — only minter or admin can mint")
56 }
57 if coinId == "" || toAddr == "" {
58 panic("coins: coinId and toAddr required")
59 }
60 if !strings.HasPrefix(toAddr, "g1") {
61 panic("coins: invalid Gno.land address")
62 }
63 amount, err := strconv.ParseUint(amountStr, 10, 64)
64 if err != nil || amount == 0 {
65 panic("coins: invalid amount")
66 }
67
68 // Credit balance
69 balKey := coinId + ":" + toAddr
70 existing := uint64(0)
71 if balances.Has(balKey) {
72 existing, _ = strconv.ParseUint(balances.Get(balKey).(string), 10, 64)
73 }
74 balances.Set(balKey, strconv.FormatUint(existing+amount, 10))
75
76 // Update total supply
77 supExisting := uint64(0)
78 if supplies.Has(coinId) {
79 supExisting, _ = strconv.ParseUint(supplies.Get(coinId).(string), 10, 64)
80 }
81 supplies.Set(coinId, strconv.FormatUint(supExisting+amount, 10))
82}
83
84// Burn destroys tokens of coinId from fromAddr.
85// Only the minter or admin can burn.
86func Burn(cur realm, coinId string, fromAddr string, amountStr string) {
87 if !isMinter(cur) && !isAdmin(cur) {
88 panic("coins: unauthorized — only minter or admin can burn")
89 }
90 amount, err := strconv.ParseUint(amountStr, 10, 64)
91 if err != nil || amount == 0 {
92 panic("coins: invalid amount")
93 }
94
95 balKey := coinId + ":" + fromAddr
96 current := uint64(0)
97 if balances.Has(balKey) {
98 current, _ = strconv.ParseUint(balances.Get(balKey).(string), 10, 64)
99 }
100 if current < amount {
101 panic("coins: insufficient balance to burn")
102 }
103 balances.Set(balKey, strconv.FormatUint(current-amount, 10))
104
105 supCurrent := uint64(0)
106 if supplies.Has(coinId) {
107 supCurrent, _ = strconv.ParseUint(supplies.Get(coinId).(string), 10, 64)
108 }
109 if supCurrent >= amount {
110 supplies.Set(coinId, strconv.FormatUint(supCurrent-amount, 10))
111 }
112}
113
114// Transfer moves tokens from the caller to toAddr.
115// Any holder can call this.
116func Transfer(cur realm, coinId string, toAddr string, amountStr string) {
117 caller := cur.Previous().Address().String()
118 if !strings.HasPrefix(toAddr, "g1") {
119 panic("coins: invalid destination address")
120 }
121 if caller == toAddr {
122 panic("coins: cannot transfer to self")
123 }
124 amount, err := strconv.ParseUint(amountStr, 10, 64)
125 if err != nil || amount == 0 {
126 panic("coins: invalid amount")
127 }
128
129 fromKey := coinId + ":" + caller
130 current := uint64(0)
131 if balances.Has(fromKey) {
132 current, _ = strconv.ParseUint(balances.Get(fromKey).(string), 10, 64)
133 }
134 if current < amount {
135 panic("coins: insufficient balance")
136 }
137
138 balances.Set(fromKey, strconv.FormatUint(current-amount, 10))
139
140 toKey := coinId + ":" + toAddr
141 toExisting := uint64(0)
142 if balances.Has(toKey) {
143 toExisting, _ = strconv.ParseUint(balances.Get(toKey).(string), 10, 64)
144 }
145 balances.Set(toKey, strconv.FormatUint(toExisting+amount, 10))
146}
147
148// ── Queries ──────────────────────────────────────────────────────────────────
149
150// BalanceOf returns the token balance of addr for coinId.
151func BalanceOf(coinId string, addr string) string {
152 key := coinId + ":" + addr
153 if !balances.Has(key) {
154 return "0"
155 }
156 return balances.Get(key).(string)
157}
158
159// BalancesOf returns all coin balances for a given address as a JSON object.
160func BalancesOf(addr string) string {
161 var parts []string
162 balances.Iterate(
163 "",
164 "\xFF",
165 func(key string, val interface{}) bool {
166 // key format: "coinId:addr"
167 idx := strings.LastIndex(key, ":")
168 if idx < 0 {
169 return false
170 }
171 balAddr := key[idx+1:]
172 if balAddr != addr {
173 return false
174 }
175 coinId := key[:idx]
176 amt := val.(string)
177 if amt != "0" {
178 parts = append(parts, ufmt.Sprintf(`"%s":"%s"`, coinId, amt))
179 }
180 return false
181 },
182 )
183 return "{" + strings.Join(parts, ",") + "}"
184}
185
186// TotalSupply returns the total minted supply for coinId.
187func TotalSupply(coinId string) string {
188 if !supplies.Has(coinId) {
189 return "0"
190 }
191 return supplies.Get(coinId).(string)
192}
193
194// MinterAddr returns the current authorized minter address.
195func MinterAddr() string {
196 return minterAddr.String()
197}
198
199func Render(_ string) string {
200 return ufmt.Sprintf(
201 "# GnoMoji Coins\n\nAdmin: `%s`\nMinter: `%s`\n\nUnified multi-token realm for all GnoMoji coins.\nUse `BalanceOf(coinId, addr)` or `BalancesOf(addr)` to query balances.\n",
202 adminAddr.String(),
203 minterAddr.String(),
204 )
205}