Search Apps Documentation Source Content File Folder Download Copy Actions Download

token.gno

9.05 Kb · 357 lines
  1package grc20
  2
  3import (
  4	"chain"
  5	"math"
  6	"math/overflow"
  7	"strconv"
  8
  9	"gno.land/p/nt/ufmt/v0"
 10)
 11
 12// NewToken creates a Token whose origRealm is bound to the calling realm.
 13// rlm must be the caller's own captured cur (asserted via rlm.IsCurrent()),
 14// and rlm.PkgPath() — the calling realm itself — becomes the Token's
 15// origRealm. Token.ID() returns origRealm + "." + symbol.
 16//
 17// Because IsCurrent runtime-validates that rlm came from the live
 18// crossing frame, origRealm is unforgeable: an external realm cannot
 19// fabricate a Token claiming to belong to a different package.
 20//
 21// Typical call from a realm's init(cur realm) or other crossing function:
 22//
 23//	Token, ledger := grc20.NewToken(0, cur, "Foo", "FOO", 4)
 24//
 25// If the Token should be discoverable, follow up with
 26// grc20reg.Register(cross, Token, slug).
 27func NewToken(_ int, rlm realm, name, symbol string, decimals int) (*Token, *PrivateLedger) {
 28	if !rlm.IsCurrent() {
 29		panic(ErrSpoofedRealm)
 30	}
 31	pkgPath := rlm.PkgPath()
 32	if pkgPath == "" {
 33		panic(ErrNotRealm)
 34	}
 35	if !validName(name) {
 36		panic(ErrInvalidName)
 37	}
 38	if !validSymbol(symbol) {
 39		panic(ErrInvalidSymbol)
 40	}
 41	if decimals < 0 || decimals > MaxDecimals {
 42		panic(ErrInvalidDecimals)
 43	}
 44	ledger := &PrivateLedger{}
 45	token := &Token{
 46		name:      name,
 47		symbol:    symbol,
 48		decimals:  decimals,
 49		origRealm: pkgPath,
 50		ledger:    ledger,
 51	}
 52	ledger.token = token
 53	return token, ledger
 54}
 55
 56// validName reports whether name is a valid display name: non-empty,
 57// within MaxNameLen, and contains no control characters (any rune
 58// below 0x20 or 0x7f). Permits Unicode letters, digits, punctuation,
 59// and spaces — name is purely a display field.
 60func validName(name string) bool {
 61	if name == "" || len(name) > MaxNameLen {
 62		return false
 63	}
 64	for _, c := range name {
 65		if c < 0x20 || c == 0x7f {
 66			return false
 67		}
 68	}
 69	return true
 70}
 71
 72// validSymbol reports whether symbol is a valid token symbol: non-empty,
 73// within MaxSymbolLen, and consists only of [A-Za-z0-9_-]. Matches
 74// grc20reg.validateSlug so the symbol round-trips cleanly through
 75// Token.ID() and any registry binding.
 76func validSymbol(symbol string) bool {
 77	if symbol == "" || len(symbol) > MaxSymbolLen {
 78		return false
 79	}
 80	for _, c := range symbol {
 81		if !isAlnum(c) && c != '_' && c != '-' {
 82			return false
 83		}
 84	}
 85	return true
 86}
 87
 88func isAlnum(c rune) bool {
 89	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
 90}
 91
 92// GetName returns the name of the token.
 93func (tok Token) GetName() string { return tok.name }
 94
 95// GetSymbol returns the symbol of the token.
 96func (tok Token) GetSymbol() string { return tok.symbol }
 97
 98// GetDecimals returns the number of decimals used to get the token's precision.
 99func (tok Token) GetDecimals() int { return tok.decimals }
100
101// TotalSupply returns the total supply of the token.
102func (tok Token) TotalSupply() int64 { return tok.ledger.totalSupply }
103
104// KnownAccounts returns the number of known accounts in the bank.
105func (tok Token) KnownAccounts() int { return tok.ledger.balances.Size() }
106
107// ID returns the Identifier of the token.
108// It is composed of the original realm and the provided symbol.
109func (tok *Token) ID() string {
110	return tok.origRealm + "." + tok.symbol
111}
112
113// HasAddr checks if the specified address is a known account in the bank.
114func (tok Token) HasAddr(addr address) bool {
115	return tok.ledger.hasAddr(addr)
116}
117
118// BalanceOf returns the balance of the specified address.
119func (tok Token) BalanceOf(addr address) int64 {
120	return tok.ledger.balanceOf(addr)
121}
122
123// Allowance returns the allowance of the specified owner and spender.
124func (tok Token) Allowance(owner, spender address) int64 {
125	return tok.ledger.allowance(owner, spender)
126}
127
128func (tok Token) RenderHome() string {
129	str := ""
130	str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol)
131	str += ufmt.Sprintf("* **Decimals**: %d\n", tok.decimals)
132	str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply)
133	str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts())
134	return str
135}
136
137// SpendAllowance decreases the allowance of the specified owner and spender.
138func (led *PrivateLedger) SpendAllowance(owner, spender address, amount int64) error {
139	if !owner.IsValid() || !spender.IsValid() {
140		return ErrInvalidAddress
141	}
142
143	if amount < 0 {
144		return ErrInvalidAmount
145	}
146	// do nothing
147	if amount == 0 {
148		return nil
149	}
150
151	currentAllowance := led.allowance(owner, spender)
152	if currentAllowance < amount {
153		return ErrInsufficientAllowance
154	}
155
156	key := allowanceKey(owner, spender)
157	newAllowance := overflow.Sub64p(currentAllowance, amount)
158
159	if newAllowance == 0 {
160		led.allowances.Remove(key)
161	} else {
162		led.allowances.Set(key, newAllowance)
163	}
164
165	return nil
166}
167
168// Transfer transfers tokens from the specified from address to the specified to address.
169func (led *PrivateLedger) Transfer(from, to address, amount int64) error {
170	if !from.IsValid() {
171		return ErrInvalidAddress
172	}
173	if !to.IsValid() {
174		return ErrInvalidAddress
175	}
176	if from == to {
177		return ErrCannotTransferToSelf
178	}
179	if amount < 0 {
180		return ErrInvalidAmount
181	}
182
183	var (
184		toBalance   = led.balanceOf(to)
185		fromBalance = led.balanceOf(from)
186	)
187
188	if fromBalance < amount {
189		return ErrInsufficientBalance
190	}
191
192	var (
193		newToBalance   = overflow.Add64p(toBalance, amount)
194		newFromBalance = overflow.Sub64p(fromBalance, amount)
195	)
196
197	led.balances.Set(string(to), newToBalance)
198
199	if newFromBalance == 0 {
200		led.balances.Remove(string(from))
201	} else {
202		led.balances.Set(string(from), newFromBalance)
203	}
204
205	chain.Emit(
206		TransferEvent,
207		"token", led.token.ID(),
208		"from", from.String(),
209		"to", to.String(),
210		"value", strconv.Itoa(int(amount)),
211	)
212
213	return nil
214}
215
216// TransferFrom transfers tokens from the specified owner to the specified to address.
217// It first checks if the owner has sufficient balance and then decreases the allowance.
218func (led *PrivateLedger) TransferFrom(owner, spender, to address, amount int64) error {
219	if amount < 0 {
220		return ErrInvalidAmount
221	}
222
223	if !owner.IsValid() || !to.IsValid() {
224		return ErrInvalidAddress
225	}
226
227	if led.balanceOf(owner) < amount {
228		return ErrInsufficientBalance
229	}
230
231	// The check above guarantees that Transfer will succeed, ensuring
232	// atomicity for the subsequent operations.
233	if err := led.SpendAllowance(owner, spender, amount); err != nil {
234		return err
235	}
236
237	if err := led.Transfer(owner, to, amount); err != nil {
238		return err
239	}
240
241	return nil
242}
243
244// Approve sets the allowance of the specified owner and spender.
245func (led *PrivateLedger) Approve(owner, spender address, amount int64) error {
246	if !owner.IsValid() || !spender.IsValid() {
247		return ErrInvalidAddress
248	}
249	if amount < 0 {
250		return ErrInvalidAmount
251	}
252
253	led.allowances.Set(allowanceKey(owner, spender), amount)
254
255	chain.Emit(
256		ApprovalEvent,
257		"token", led.token.ID(),
258		"owner", string(owner),
259		"spender", string(spender),
260		"value", strconv.Itoa(int(amount)),
261	)
262
263	return nil
264}
265
266// Mint increases the total supply of the token and adds the specified amount to the specified address.
267func (led *PrivateLedger) Mint(addr address, amount int64) error {
268	if !addr.IsValid() {
269		return ErrInvalidAddress
270	}
271	if amount < 0 {
272		return ErrInvalidAmount
273	}
274
275	// limit amount to MaxInt64 - totalSupply
276	if amount > overflow.Sub64p(math.MaxInt64, led.totalSupply) {
277		return ErrMintOverflow
278	}
279
280	led.totalSupply += amount
281	currentBalance := led.balanceOf(addr)
282	newBalance := overflow.Add64p(currentBalance, amount)
283
284	led.balances.Set(string(addr), newBalance)
285
286	chain.Emit(
287		TransferEvent,
288		"token", led.token.ID(),
289		"from", "",
290		"to", string(addr),
291		"value", strconv.Itoa(int(amount)),
292	)
293
294	return nil
295}
296
297// Burn decreases the total supply of the token and subtracts the specified amount from the specified address.
298func (led *PrivateLedger) Burn(addr address, amount int64) error {
299	if !addr.IsValid() {
300		return ErrInvalidAddress
301	}
302	if amount < 0 {
303		return ErrInvalidAmount
304	}
305
306	currentBalance := led.balanceOf(addr)
307	if currentBalance < amount {
308		return ErrInsufficientBalance
309	}
310
311	led.totalSupply = overflow.Sub64p(led.totalSupply, amount)
312	newBalance := overflow.Sub64p(currentBalance, amount)
313
314	if newBalance == 0 {
315		led.balances.Remove(string(addr))
316	} else {
317		led.balances.Set(string(addr), newBalance)
318	}
319
320	chain.Emit(
321		TransferEvent,
322		"token", led.token.ID(),
323		"from", string(addr),
324		"to", "",
325		"value", strconv.Itoa(int(amount)),
326	)
327
328	return nil
329}
330
331// hasAddr checks if the specified address is a known account in the ledger.
332func (led PrivateLedger) hasAddr(addr address) bool {
333	return led.balances.Has(addr.String())
334}
335
336// balanceOf returns the balance of the specified address.
337func (led PrivateLedger) balanceOf(addr address) int64 {
338	balance, found := led.balances.Get(addr.String())
339	if !found {
340		return 0
341	}
342	return balance.(int64)
343}
344
345// allowance returns the allowance of the specified owner and spender.
346func (led PrivateLedger) allowance(owner, spender address) int64 {
347	allowance, found := led.allowances.Get(allowanceKey(owner, spender))
348	if !found {
349		return 0
350	}
351	return allowance.(int64)
352}
353
354// allowanceKey returns the key for the allowance of the specified owner and spender.
355func allowanceKey(owner, spender address) string {
356	return owner.String() + ":" + spender.String()
357}