token.gno

// Package token is a fixed-supply GRC20-shaped fungible token realm.
//
// The full supply (TotalSupplyConst) is minted once, to the deploying
// address, inside init() — there is no ongoing or public Mint function
// afterward, and no way to create more supply later.
//
// Deliberately not a dependency on gno.land/p/demo/tokens/grc20: that
// package's NewToken constructor, Teller interface, and Token.ID()
// collision-safety were all found to diverge between gno.land networks
// (confirmed directly against each chain's deployed source, not
// assumed — see ~/gno-land-dev-notes.md's discovery log). Everything
// here is self-implemented on a vendored, network-agnostic avl.Tree
// instead — see gno.land/p/<addr>/avl/tree.gno's own header comment for
// why that package is vendored rather than imported from
// gno.land/p/nt/avl/v0.
package token

import (
	unsaferealm "chain/runtime/unsafe"
	"chain"
	"errors"
	"strconv"

	"gno.land/p/g1avv0u2d45d8lf8ywqxt8t867npwp4zvdxw2fl2/avl"
)

const (
	MintEvent     = "Mint"
	TransferEvent = "Transfer"
	ApprovalEvent = "Approval"
)

const (
	TokenName     = "Sample Token"
	TokenSymbol   = "SAMPLE"
	TokenDecimals = 6

	TotalSupplyConst int64 = 1_000_000
)

var (
	creator address = "g1avv0u2d45d8lf8ywqxt8t867npwp4zvdxw2fl2"

	balances   avl.Tree // address string -> int64
	allowances avl.Tree // "owner:spender" string -> int64
)

var (
	ErrInsufficientBalance   = errors.New("insufficient balance")
	ErrInsufficientAllowance = errors.New("insufficient allowance")
	ErrInvalidAddress        = errors.New("invalid address")
	ErrCannotTransferToSelf  = errors.New("cannot transfer to self")
	ErrInvalidAmount         = errors.New("invalid amount")
)

func init(cur realm) {
	balances.Set(creator.String(), TotalSupplyConst)
	chain.Emit(
		MintEvent,
		"to", creator.String(),
		"amount", strconv.FormatInt(TotalSupplyConst, 10),
	)
}

func callerAddress() address {
	return unsaferealm.PreviousRealm().Address()
}

func Name() string       { return TokenName }
func Symbol() string     { return TokenSymbol }
func Decimals() int      { return TokenDecimals }
func TotalSupply() int64 { return TotalSupplyConst }

func BalanceOf(addr address) int64 {
	v := balances.Get(addr.String())
	if v == nil {
		return 0
	}
	return v.(int64)
}

func Allowance(owner, spender address) int64 {
	v := allowances.Get(allowanceKey(owner, spender))
	if v == nil {
		return 0
	}
	return v.(int64)
}

func allowanceKey(owner, spender address) string {
	return owner.String() + ":" + spender.String()
}

func Transfer(cur realm, to address, amount int64) error {
	return transfer(callerAddress(), to, amount)
}

func Approve(cur realm, spender address, amount int64) error {
	owner := callerAddress()
	if !spender.IsValid() {
		return ErrInvalidAddress
	}
	if amount < 0 {
		return ErrInvalidAmount
	}
	allowances.Set(allowanceKey(owner, spender), amount)
	chain.Emit(
		ApprovalEvent,
		"owner", owner.String(),
		"spender", spender.String(),
		"amount", strconv.FormatInt(amount, 10),
	)
	return nil
}

func TransferFrom(cur realm, from, to address, amount int64) error {
	spender := callerAddress()
	current := Allowance(from, spender)
	if current < amount {
		return ErrInsufficientAllowance
	}
	if err := transfer(from, to, amount); err != nil {
		return err
	}
	allowances.Set(allowanceKey(from, spender), current-amount)
	return nil
}

func transfer(from, to address, amount int64) error {
	if !to.IsValid() {
		return ErrInvalidAddress
	}
	if from == to {
		return ErrCannotTransferToSelf
	}
	if amount <= 0 {
		return ErrInvalidAmount
	}
	fromBal := BalanceOf(from)
	if fromBal < amount {
		return ErrInsufficientBalance
	}
	balances.Set(from.String(), fromBal-amount)
	balances.Set(to.String(), BalanceOf(to)+amount)
	chain.Emit(
		TransferEvent,
		"from", from.String(),
		"to", to.String(),
		"amount", strconv.FormatInt(amount, 10),
	)
	return nil
}

func Render(path string) string {
	b := "# " + TokenName + " (" + TokenSymbol + ")\n\n"
	b += "* **Decimals**: " + strconv.Itoa(TokenDecimals) + "\n"
	b += "* **Total supply**: " + strconv.FormatInt(TotalSupply(), 10) + "\n"
	b += "* **Creator**: " + creator.String() + "\n"
	return b
}