From 0caee282ea24af47f3afe50a9235a7e2d274948b Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 30 May 2024 17:59:56 +0200 Subject: [PATCH] feat(examples): add grc20factory (#1913) Signed-off-by: moul <94029+moul@users.noreply.github.com>
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/r/demo/grc20factory/gno.mod | 6 + .../r/demo/grc20factory/grc20factory.gno | 164 ++++++++++++++++++ .../r/demo/grc20factory/grc20factory_test.gno | 58 +++++++ 3 files changed, 228 insertions(+) create mode 100644 examples/gno.land/r/demo/grc20factory/gno.mod create mode 100644 examples/gno.land/r/demo/grc20factory/grc20factory.gno create mode 100644 examples/gno.land/r/demo/grc20factory/grc20factory_test.gno diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod new file mode 100644 index 000000000000..a430a1f9559c --- /dev/null +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/demo/grc20factory + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/grc/grc20 v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory.gno b/examples/gno.land/r/demo/grc20factory/grc20factory.gno new file mode 100644 index 000000000000..0146d945f0d5 --- /dev/null +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -0,0 +1,164 @@ +package foo20 + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc20" +) + +// XXX: the p/grc20 library needs a refactor to change names (i.e., adminToken) + +type token struct { + adminToken *grc20.AdminToken + admin std.Address +} + +var tokens avl.Tree // symbol -> token + +func NewToken(name, symbol string, decimals uint, initialMint uint64) { + admin := std.PrevRealm().Addr() + NewTokenWithAdmin(name, symbol, decimals, initialMint, admin) +} + +func NewTokenWithAdmin(name, symbol string, decimals uint, initialMint uint64, admin std.Address) { + exists := tokens.Has(symbol) + if exists { + panic("token already exists") + } + + newToken := grc20.NewAdminToken(name, symbol, decimals) + newToken.Mint(admin, initialMint) + t := token{ + adminToken: newToken, + admin: admin, + } + tokens.Set(symbol, &t) +} + +// method proxies as public functions. +// + +// getters. + +func TotalSupply(symbol string) uint64 { + token := mustTokenBySymbol(symbol) + return token.adminToken.TotalSupply() +} + +func BalanceOf(symbol string, owner std.Address) uint64 { + token := mustTokenBySymbol(symbol) + balance, err := token.adminToken.BalanceOf(owner) + checkErr(err) + return balance +} + +func Allowance(symbol string, owner, spender std.Address) uint64 { + token := mustTokenBySymbol(symbol) + allowance, err := token.adminToken.Allowance(owner, spender) + checkErr(err) + return allowance +} + +// setters. + +func Transfer(symbol string, to std.Address, amount uint64) { + token := mustTokenBySymbol(symbol) + caller := std.PrevRealm().Addr() + err := token.adminToken.Transfer(caller, to, amount) + checkErr(err) +} + +func Approve(symbol string, spender std.Address, amount uint64) { + token := mustTokenBySymbol(symbol) + caller := std.PrevRealm().Addr() + err := token.adminToken.Approve(caller, spender, amount) + checkErr(err) +} + +func TransferFrom(symbol string, from, to std.Address, amount uint64) { + token := mustTokenBySymbol(symbol) + caller := std.PrevRealm().Addr() + err := token.adminToken.TransferFrom(caller, from, to, amount) + if err != nil { + panic(err) + } +} + +// faucet. +func Faucet(symbol string) { + token := mustTokenBySymbol(symbol) + // FIXME: add limits? + // FIXME: add payment in gnot? + caller := std.PrevRealm().Addr() + err := token.adminToken.Mint(caller, 1000*10000) // 1k + checkErr(err) +} + +// administration. + +func Mint(symbol string, address std.Address, amount uint64) { + token := mustTokenBySymbol(symbol) + caller := std.PrevRealm().Addr() + assertIsAdmin(caller, token.admin) + err := token.adminToken.Mint(address, amount) + checkErr(err) +} + +func Burn(symbol string, address std.Address, amount uint64) { + token := mustTokenBySymbol(symbol) + caller := std.PrevRealm().Addr() + assertIsAdmin(caller, token.admin) + err := token.adminToken.Burn(address, amount) + checkErr(err) +} + +// render. +// + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return "TODO: list existing tokens and admins" + case c == 1: + symbol := parts[0] + token := mustTokenBySymbol(symbol) + return token.adminToken.RenderHome() + case c == 3 && parts[1] == "balance": + symbol := parts[0] + token := mustTokenBySymbol(symbol) + owner := std.Address(parts[2]) + balance, _ := token.adminToken.BalanceOf(owner) + return strconv.FormatUint(balance, 10) + default: + return "404\n" + } +} + +// helpers. +// + +func assertIsAdmin(caller, admin std.Address) { + if caller != admin { + panic("restricted access") + } +} + +func mustTokenBySymbol(symbol string) *token { + t, exists := tokens.Get(symbol) + if !exists { + panic("token does not exist") + } + return t.(*token) +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno new file mode 100644 index 000000000000..fb1830ecef8c --- /dev/null +++ b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno @@ -0,0 +1,58 @@ +package foo20 + +import ( + "std" + "testing" +) + +func TestReadOnlyPublicMethods(t *testing.T) { + admin := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") + manfred := std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + unknown := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // valid but never used. + NewTokenWithAdmin("Foo", "FOO", 4, 10000000000, admin) + NewTokenWithAdmin("Bar", "BAR", 4, 10000000, admin) + mustTokenBySymbol("FOO").adminToken.Mint(manfred, 100000000) + + type test struct { + name string + balance uint64 + fn func() uint64 + } + + // check balances #1. + { + tests := []test{ + {"TotalSupply", 10100000000, func() uint64 { return TotalSupply("FOO") }}, + {"BalanceOf(admin)", 10000000000, func() uint64 { return BalanceOf("FOO", admin) }}, + {"BalanceOf(manfred)", 100000000, func() uint64 { return BalanceOf("FOO", manfred) }}, + {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance("FOO", admin, manfred) }}, + {"BalanceOf(unknown)", 0, func() uint64 { return BalanceOf("FOO", unknown) }}, + } + for _, tc := range tests { + if tc.fn() != tc.balance { + t.Errorf("%s: have: %d want: %d", tc.name, tc.fn(), tc.balance) + } + } + } + return + + // unknown uses the faucet. + std.TestSetOrigCaller(unknown) + Faucet("FOO") + + // check balances #2. + { + tests := []test{ + {"TotalSupply", 10110000000, func() uint64 { return TotalSupply("FOO") }}, + {"BalanceOf(admin)", 10000000000, func() uint64 { return BalanceOf("FOO", admin) }}, + {"BalanceOf(manfred)", 100000000, func() uint64 { return BalanceOf("FOO", manfred) }}, + {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance("FOO", admin, manfred) }}, + {"BalanceOf(unknown)", 10000000, func() uint64 { return BalanceOf("FOO", unknown) }}, + } + for _, tc := range tests { + if tc.fn() != tc.balance { + t.Errorf("%s: have: %d want: %d", tc.name, tc.fn(), tc.balance) + } + } + } +}