grc20factory.gno

package foo20

import (
	"chain/runtime"

	"gno.land/p/demo/tokens/grc20"
	"gno.land/p/moul/md"
	"gno.land/p/nt/avl/v0"
	p "gno.land/p/nt/avl/v0/pager"
	"gno.land/p/nt/avl/v0/rotree"
	"gno.land/p/nt/mux/v0"
	"gno.land/p/nt/ownable/v0"
	"gno.land/p/nt/ufmt/v0"
	"gno.land/r/demo/defi/grc20reg"
)

var (
	instances avl.Tree // symbol -> *instance
	pager     = p.NewPager(rotree.Wrap(&instances, nil), 20, false)
)

type instance struct {
	token  *grc20.Token
	ledger *grc20.PrivateLedger
	admin  *ownable.Ownable
	faucet int64 // per-request amount. disabled if 0.
}

func New(cur realm, name, symbol string, decimals int, initialMint, faucet int64) {
	caller := runtime.PreviousRealm().Address()
	NewWithAdmin(cur, name, symbol, decimals, initialMint, faucet, caller)
}

func NewWithAdmin(cur realm, name, symbol string, decimals int, initialMint, faucet int64, admin address) {
	exists := instances.Has(symbol)
	if exists {
		panic("token already exists")
	}

	token, ledger := grc20.NewToken(name, symbol, decimals)
	if initialMint > 0 {
		ledger.Mint(admin, initialMint)
	}

	inst := instance{
		token:  token,
		ledger: ledger,
		admin:  ownable.NewWithAddressByPrevious(admin),
		faucet: faucet,
	}
	instances.Set(symbol, &inst)

	grc20reg.Register(cross, token, symbol)
}

func Bank(symbol string) *grc20.Token {
	inst := mustGetInstance(symbol)
	return inst.token
}

func TotalSupply(symbol string) int64 {
	inst := mustGetInstance(symbol)
	return inst.token.ReadonlyTeller().TotalSupply()
}

func HasAddr(symbol string, owner address) bool {
	inst := mustGetInstance(symbol)
	return inst.token.HasAddr(owner)
}

func BalanceOf(symbol string, owner address) int64 {
	inst := mustGetInstance(symbol)
	return inst.token.ReadonlyTeller().BalanceOf(owner)
}

func Allowance(symbol string, owner, spender address) int64 {
	inst := mustGetInstance(symbol)
	return inst.token.ReadonlyTeller().Allowance(owner, spender)
}

func Transfer(cur realm, symbol string, to address, amount int64) {
	inst := mustGetInstance(symbol)
	caller := runtime.PreviousRealm().Address()
	teller := inst.ledger.ImpersonateTeller(caller)
	checkErr(teller.Transfer(to, amount))
}

func Approve(cur realm, symbol string, spender address, amount int64) {
	inst := mustGetInstance(symbol)
	caller := runtime.PreviousRealm().Address()
	teller := inst.ledger.ImpersonateTeller(caller)
	checkErr(teller.Approve(spender, amount))
}

func TransferFrom(cur realm, symbol string, from, to address, amount int64) {
	inst := mustGetInstance(symbol)
	caller := runtime.PreviousRealm().Address()
	teller := inst.ledger.ImpersonateTeller(caller)
	checkErr(teller.TransferFrom(from, to, amount))
}

// faucet.
func Faucet(cur realm, symbol string) {
	inst := mustGetInstance(symbol)
	if inst.faucet == 0 {
		panic("faucet disabled for this token")
	}
	// FIXME: add limits?
	// FIXME: add payment in gnot?
	caller := runtime.PreviousRealm().Address()
	checkErr(inst.ledger.Mint(caller, inst.faucet))
}

func Mint(cur realm, symbol string, to address, amount int64) {
	inst := mustGetInstance(symbol)
	inst.admin.AssertOwned()
	checkErr(inst.ledger.Mint(to, amount))
}

func Burn(cur realm, symbol string, from address, amount int64) {
	inst := mustGetInstance(symbol)
	inst.admin.AssertOwned()
	checkErr(inst.ledger.Burn(from, amount))
}

// instance admin functionality
func DropInstanceOwnership(cur realm, symbol string) {
	inst := mustGetInstance(symbol)
	checkErr(inst.admin.DropOwnership())
}

func TransferInstanceOwnership(cur realm, symbol string, newOwner address) {
	inst := mustGetInstance(symbol)
	checkErr(inst.admin.TransferOwnership(newOwner))
}

func ListTokens(pageNumber, pageSize int) []*grc20.Token {
	page := pager.GetPageWithSize(pageNumber, pageSize)

	tokens := make([]*grc20.Token, len(page.Items))
	for i := range page.Items {
		tokens[i] = page.Items[i].Value.(*instance).token
	}

	return tokens
}

func Render(path string) string {
	router := mux.NewRouter()
	router.HandleFunc("", renderHome)
	router.HandleFunc("{symbol}", renderToken)
	router.HandleFunc("{symbol}/balance/{address}", renderBalance)
	return router.Render(path)
}

func renderHome(res *mux.ResponseWriter, req *mux.Request) {
	out := md.H1(ufmt.Sprintf("GRC20 Tokens (%d)", instances.Size()))

	// Get the current page of tokens based on the request path.
	page := pager.MustGetPageByPath(req.RawPath)

	// Render the list of tokens.
	for _, item := range page.Items {
		token := item.Value.(*instance).token
		out += md.BulletItem(
			md.Link(
				ufmt.Sprintf("%s ($%s)", token.GetName(), token.GetSymbol()),
				ufmt.Sprintf("/r/demo/grc20factory:%s", token.GetSymbol()),
			),
		)
	}
	out += "\n"

	// Add the page picker.
	out += md.Paragraph(page.Picker(req.Path))

	res.Write(out)
}

func renderToken(res *mux.ResponseWriter, req *mux.Request) {
	// Get the token symbol from the request.
	symbol := req.GetVar("symbol")
	inst := mustGetInstance(symbol)

	// Render the token details.
	out := inst.token.RenderHome()
	out += md.BulletItem(
		ufmt.Sprintf("%s: %s", md.Bold("Admin"), inst.admin.Owner()),
	)

	res.Write(out)
}

func renderBalance(res *mux.ResponseWriter, req *mux.Request) {
	var (
		symbol = req.GetVar("symbol")
		addr   = req.GetVar("address")
	)

	// Get the balance of the specified address for the token.
	inst := mustGetInstance(symbol)
	balance := inst.token.CallerTeller().BalanceOf(address(addr))

	// Render the balance information.
	out := md.Paragraph(
		ufmt.Sprintf("%s balance: %d", md.Bold(addr), balance),
	)

	res.Write(out)
}

func mustGetInstance(symbol string) *instance {
	t, exists := instances.Get(symbol)
	if !exists {
		panic("token instance does not exist")
	}
	return t.(*instance)
}

func checkErr(err error) {
	if err != nil {
		panic(err.Error())
	}
}