From a885c78da2ef749b711241b578b525f9bf4155a1 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:06:05 +0100 Subject: [PATCH 01/60] fix(gnovm/softfloat): replace copy.sh with Go generator (#3584) Co-authored-by: Morgan Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/pkg/gnolang/internal/softfloat/copy.sh | 32 ----- .../gnolang/internal/softfloat/gen/main.go | 116 ++++++++++++++++++ .../internal/softfloat/runtime_softfloat64.go | 2 +- .../softfloat/runtime_softfloat64_test.go | 6 +- .../gnolang/internal/softfloat/softfloat.go | 2 +- 5 files changed, 121 insertions(+), 37 deletions(-) delete mode 100644 gnovm/pkg/gnolang/internal/softfloat/copy.sh create mode 100644 gnovm/pkg/gnolang/internal/softfloat/gen/main.go diff --git a/gnovm/pkg/gnolang/internal/softfloat/copy.sh b/gnovm/pkg/gnolang/internal/softfloat/copy.sh deleted file mode 100644 index 6d2a8f80462..00000000000 --- a/gnovm/pkg/gnolang/internal/softfloat/copy.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh - -# softfloat64.go: -# - add header -# - change package name -cat > runtime_softfloat64.go << EOF -// Code generated by copy.sh. DO NOT EDIT. -// This file is copied from \$GOROOT/src/runtime/softfloat64.go. -// It is the software floating point implementation used by the Go runtime. - -EOF -cat "$GOROOT/src/runtime/softfloat64.go" >> ./runtime_softfloat64.go -sed -i 's/^package runtime$/package softfloat/' runtime_softfloat64.go - -# softfloat64_test.go: -# - add header -# - change package name -# - change import to right package -# - change GOARCH to runtime.GOARCH, and import the "runtime" package -cat > runtime_softfloat64_test.go << EOF -// Code generated by copy.sh. DO NOT EDIT. -// This file is copied from \$GOROOT/src/runtime/softfloat64_test.go. -// It is the tests for the software floating point implementation -// used by the Go runtime. - -EOF -cat "$GOROOT/src/runtime/softfloat64_test.go" >> ./runtime_softfloat64_test.go -sed -i 's/^package runtime_test$/package softfloat_test/ -s#^\t\. "runtime"$#\t. "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat"# -s/GOARCH/runtime.GOARCH/g -16a\ - "runtime"' runtime_softfloat64_test.go \ No newline at end of file diff --git a/gnovm/pkg/gnolang/internal/softfloat/gen/main.go b/gnovm/pkg/gnolang/internal/softfloat/gen/main.go new file mode 100644 index 00000000000..7c89ff9b5a9 --- /dev/null +++ b/gnovm/pkg/gnolang/internal/softfloat/gen/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +func main() { + // Process softfloat64.go file + processSoftFloat64File() + + // Process softfloat64_test.go file + processSoftFloat64TestFile() + + // Run mvdan.cc/gofumpt + gofumpt() + + fmt.Println("Files processed successfully.") +} + +func processSoftFloat64File() { + // Read source file + content, err := os.ReadFile(fmt.Sprintf("%s/src/runtime/softfloat64.go", runtime.GOROOT())) + if err != nil { + log.Fatal("Error reading source file:", err) + } + + // Prepare header + header := `// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. +// This file is copied from $GOROOT/src/runtime/softfloat64.go. +// It is the software floating point implementation used by the Go runtime. + +` + + // Combine header with content + newContent := header + string(content) + + // Replace package name + newContent = strings.Replace(newContent, "package runtime", "package softfloat", 1) + + // Write to destination file + err = os.WriteFile("runtime_softfloat64.go", []byte(newContent), 0o644) + if err != nil { + log.Fatal("Error writing to destination file:", err) + } +} + +func processSoftFloat64TestFile() { + // Read source test file + content, err := os.ReadFile(fmt.Sprintf("%s/src/runtime/softfloat64_test.go", runtime.GOROOT())) + if err != nil { + log.Fatal("Error reading source test file:", err) + } + + // Prepare header + header := `// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. +// This file is copied from $GOROOT/src/runtime/softfloat64_test.go. +// It is the tests for the software floating point implementation +// used by the Go runtime. + +` + + // Combine header with content + newContent := header + string(content) + + // Replace package name and imports + newContent = strings.Replace(newContent, "package runtime_test", "package softfloat_test", 1) + newContent = strings.Replace(newContent, "\t. \"runtime\"", "\t\"runtime\"", 1) + newContent = strings.Replace(newContent, "GOARCH", "runtime.GOARCH", 1) + + newContent = strings.Replace(newContent, "import (", "import (\n\t. \"github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat\"", 1) + + // Write to destination file + err = os.WriteFile("runtime_softfloat64_test.go", []byte(newContent), 0o644) + if err != nil { + log.Fatal("Error writing to destination test file:", err) + } +} + +func gitRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + p := wd + for { + if s, e := os.Stat(filepath.Join(p, ".git")); e == nil && s.IsDir() { + return p, nil + } + + if strings.HasSuffix(p, string(filepath.Separator)) { + return "", errors.New("root git not found") + } + + p = filepath.Dir(p) + } +} + +func gofumpt() { + rootPath, err := gitRoot() + if err != nil { + log.Fatal("error finding git root:", err) + } + + cmd := exec.Command("go", "run", "-modfile", filepath.Join(strings.TrimSpace(rootPath), "misc/devdeps/go.mod"), "mvdan.cc/gofumpt", "-w", ".") + _, err = cmd.Output() + if err != nil { + log.Fatal("error gofumpt:", err) + } +} diff --git a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go index cf2ad5afd8a..7623b9c2077 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go +++ b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go @@ -1,4 +1,4 @@ -// Code generated by copy.sh. DO NOT EDIT. +// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. // This file is copied from $GOROOT/src/runtime/softfloat64.go. // It is the software floating point implementation used by the Go runtime. diff --git a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go index c57fe08b0ef..8b5d34650f1 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go +++ b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go @@ -1,4 +1,4 @@ -// Code generated by copy.sh. DO NOT EDIT. +// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. // This file is copied from $GOROOT/src/runtime/softfloat64_test.go. // It is the tests for the software floating point implementation // used by the Go runtime. @@ -10,11 +10,11 @@ package softfloat_test import ( + . "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" "math" "math/rand" - . "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" + "runtime" "testing" - "runtime" ) // turn uint64 op into float64 op diff --git a/gnovm/pkg/gnolang/internal/softfloat/softfloat.go b/gnovm/pkg/gnolang/internal/softfloat/softfloat.go index 30f66dff620..89dcd04d8fb 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/softfloat.go +++ b/gnovm/pkg/gnolang/internal/softfloat/softfloat.go @@ -17,7 +17,7 @@ package softfloat // This file mostly exports the functions from runtime_softfloat64.go -//go:generate sh copy.sh +//go:generate go run github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen const ( mask = 0x7FF From df4113d75450066a72c627f584686b28d2a10cbc Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:44:49 +0100 Subject: [PATCH 02/60] feat(examples): update leon's config & home (#3603) --- examples/gno.land/r/leon/config/config.gno | 119 +++++++++++++++------ examples/gno.land/r/leon/hof/hof.gno | 22 ++-- examples/gno.land/r/leon/home/home.gno | 42 ++++---- 3 files changed, 119 insertions(+), 64 deletions(-) diff --git a/examples/gno.land/r/leon/config/config.gno b/examples/gno.land/r/leon/config/config.gno index bc800ec8263..bb90a6c21d7 100644 --- a/examples/gno.land/r/leon/config/config.gno +++ b/examples/gno.land/r/leon/config/config.gno @@ -3,61 +3,116 @@ package config import ( "errors" "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/avl" + p "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/p/moul/realmpath" ) var ( - main std.Address // leon's main address - backup std.Address // backup address + configs = avl.NewTree() + pager = p.NewPager(configs, 10, false) + banner = "---\n[[Leon's Home page]](/r/leon/home) | [[GitHub: @leohhhn]](https://github.com/leohhhn)\n\n---" + absPath = strings.TrimPrefix(std.CurrentRealm().PkgPath(), std.GetChainDomain()) + + // SafeObjects + OwnableMain = ownable.NewWithAddress("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") + OwnableBackup = ownable.NewWithAddress("g1lavlav7zwsjqlzzl3qdl3nl242qtf638vnhdjh") - ErrInvalidAddr = errors.New("leon's config: invalid address") ErrUnauthorized = errors.New("leon's config: unauthorized") ) -func init() { - main = "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5" +type Config struct { + lines string + updated time.Time } -func Address() std.Address { - return main -} +func AddConfig(name, lines string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) + } -func Backup() std.Address { - return backup + configs.Set(name, Config{ + lines: lines, + updated: time.Now(), + }) // no overwrite check } -func SetAddress(a std.Address) error { - if !a.IsValid() { - return ErrInvalidAddr +func RemoveConfig(name string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) } - if err := checkAuthorized(); err != nil { - return err + if _, ok := configs.Remove(name); !ok { + panic("no config with that name") } - - main = a - return nil } -func SetBackup(a std.Address) error { - if !a.IsValid() { - return ErrInvalidAddr +func UpdateBanner(newBanner string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) } - if err := checkAuthorized(); err != nil { - return err - } + banner = newBanner +} - backup = a - return nil +func IsAuthorized(addr std.Address) bool { + return addr == OwnableMain.Owner() || addr == OwnableBackup.Owner() } -func checkAuthorized() error { - caller := std.PrevRealm().Addr() - isAuthorized := caller == main || caller == backup +func Banner() string { + return banner +} + +func Render(path string) (out string) { + req := realmpath.Parse(path) + if req.Path == "" { + out += md.H1("Leon's config package") + + out += ufmt.Sprintf("Leon's main address: %s\n\n", OwnableMain.Owner().String()) + out += ufmt.Sprintf("Leon's backup address: %s\n\n", OwnableBackup.Owner().String()) - if !isAuthorized { - return ErrUnauthorized + out += md.H2("Leon's configs") + + if configs.Size() == 0 { + out += "No configs yet :c\n\n" + } + + page := pager.MustGetPageByPath(path) + for _, item := range page.Items { + out += ufmt.Sprintf("- [%s](%s:%s)\n\n", item.Key, absPath, item.Key) + } + + out += page.Picker() + out += "\n\n" + out += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" + + out += Banner() + + return out } - return nil + return renderConfPage(req.Path) +} + +func renderConfPage(confName string) (out string) { + raw, ok := configs.Get(confName) + if !ok { + out += md.H1("404") + out += "That config does not exist :/" + return out + } + + conf := raw.(Config) + out += md.H1(confName) + out += ufmt.Sprintf("```\n%s\n```\n\n", conf.lines) + out += ufmt.Sprintf("_Last updated on %s_", conf.updated.Format("02 Jan, 2006")) + + return out } diff --git a/examples/gno.land/r/leon/hof/hof.gno b/examples/gno.land/r/leon/hof/hof.gno index 147a0dd1a95..96266ffe380 100644 --- a/examples/gno.land/r/leon/hof/hof.gno +++ b/examples/gno.land/r/leon/hof/hof.gno @@ -10,6 +10,8 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/pausable" "gno.land/p/demo/seqid" + + "gno.land/r/leon/config" ) var ( @@ -24,7 +26,7 @@ type ( Exhibition struct { itemCounter seqid.ID description string - items *avl.Tree // pkgPath > Item + items *avl.Tree // pkgPath > &Item itemsSorted *avl.Tree // same data but sorted, storing pointers } @@ -43,7 +45,7 @@ func init() { itemsSorted: avl.NewTree(), } - Ownable = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5")) + Ownable = ownable.NewWithAddress(config.OwnableMain.Owner()) // OrigSendOwnable? Pausable = pausable.NewFromOwnable(Ownable) } @@ -85,14 +87,14 @@ func Register() { func Upvote(pkgpath string) { rawItem, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } item := rawItem.(*Item) caller := std.PrevRealm().Addr().String() if item.upvote.Has(caller) { - panic(ErrDoubleUpvote.Error()) + panic(ErrDoubleUpvote) } item.upvote.Set(caller, struct{}{}) @@ -101,14 +103,14 @@ func Upvote(pkgpath string) { func Downvote(pkgpath string) { rawItem, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } item := rawItem.(*Item) caller := std.PrevRealm().Addr().String() if item.downvote.Has(caller) { - panic(ErrDoubleDownvote.Error()) + panic(ErrDoubleDownvote) } item.downvote.Set(caller, struct{}{}) @@ -116,19 +118,19 @@ func Downvote(pkgpath string) { func Delete(pkgpath string) { if !Ownable.CallerIsOwner() { - panic(ownable.ErrUnauthorized.Error()) + panic(ownable.ErrUnauthorized) } i, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } if _, removed := exhibition.items.Remove(pkgpath); !removed { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } } diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index cf33260cc6b..aef261fcd60 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -19,7 +19,24 @@ var ( abtMe [2]string ) +func Render(path string) string { + out := "# Leon's Homepage\n\n" + + out += renderAboutMe() + out += renderBlogPosts() + out += "\n\n" + out += renderArt() + out += "\n\n" + out += config.Banner() + out += "\n\n" + + return out +} + func init() { + hof.Register() + mirror.Register(std.CurrentRealm().PkgPath(), Render) + pfp = "https://i.imgflip.com/91vskx.jpg" pfpCaption = "[My favourite painting & pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)" abtMe = [2]string{ @@ -30,16 +47,12 @@ life-long learner, and sharer of knowledge.`, My contributions to gno.land can mainly be found [here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn). -TODO import r/gh -`, +TODO import r/gh`, } - - hof.Register() - mirror.Register(std.CurrentRealm().PkgPath(), Render) } func UpdatePFP(url, caption string) { - if !isAuthorized(std.PrevRealm().Addr()) { + if !config.IsAuthorized(std.PrevRealm().Addr()) { panic(config.ErrUnauthorized) } @@ -48,7 +61,7 @@ func UpdatePFP(url, caption string) { } func UpdateAboutMe(col1, col2 string) { - if !isAuthorized(std.PrevRealm().Addr()) { + if !config.IsAuthorized(std.PrevRealm().Addr()) { panic(config.ErrUnauthorized) } @@ -56,17 +69,6 @@ func UpdateAboutMe(col1, col2 string) { abtMe[1] = col2 } -func Render(path string) string { - out := "# Leon's Homepage\n\n" - - out += renderAboutMe() - out += renderBlogPosts() - out += "\n\n" - out += renderArt() - - return out -} - func renderBlogPosts() string { out := "" //out += "## Leon's Blog Posts" @@ -130,7 +132,3 @@ func renderMillipede() string { return out } - -func isAuthorized(addr std.Address) bool { - return addr == config.Address() || addr == config.Backup() -} From 21fe65624a39fce3c589c1dd2d897b02b720f292 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:55:05 +0100 Subject: [PATCH 03/60] feat(r/docs): pager + render paths (#3608) --- .../r/docs/avl_pager_with_params/gno.mod | 1 + .../r/docs/avl_pager_with_params/render.gno | 86 +++++++++++++++++++ examples/gno.land/r/docs/docs.gno | 1 + 3 files changed, 88 insertions(+) create mode 100644 examples/gno.land/r/docs/avl_pager_with_params/gno.mod create mode 100644 examples/gno.land/r/docs/avl_pager_with_params/render.gno diff --git a/examples/gno.land/r/docs/avl_pager_with_params/gno.mod b/examples/gno.land/r/docs/avl_pager_with_params/gno.mod new file mode 100644 index 00000000000..aeb5b047762 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager_with_params/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/avl_pager_params diff --git a/examples/gno.land/r/docs/avl_pager_with_params/render.gno b/examples/gno.land/r/docs/avl_pager_with_params/render.gno new file mode 100644 index 00000000000..108f5735b65 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager_with_params/render.gno @@ -0,0 +1,86 @@ +package avl_pager_params + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/realmpath" +) + +// We'll keep some demo data in an AVL tree to showcase pagination. +var ( + items *avl.Tree + idCounter seqid.ID +) + +func init() { + items = avl.NewTree() + // Populate the tree with 15 sample items for demonstration. + for i := 1; i <= 15; i++ { + id := idCounter.Next().String() + items.Set(id, "Some item value: "+id) + } +} + +func Render(path string) string { + // 1) Parse the incoming path to split route vs. query. + req := realmpath.Parse(path) + // - req.Path contains everything *before* ? or $ (? - query params, $ - gnoweb params) + // - The remaining part (page=2, size=5, etc.) is not in req.Path. + + // 2) If no specific route is provided (req.Path == ""), we’ll show a “home” page + // that displays a list of configs in paginated form. + if req.Path == "" { + return renderHome(path) + } + + // 3) If a route *is* provided (e.g. :SomeKey), + // we will interpret it as a request for a specific page. + return renderConfigItem(req.Path) +} + +// renderHome shows a paginated list of config items if route == "". +func renderHome(fullPath string) string { + // Create a Pager for our config tree, with a default page size of 5. + p := pager.NewPager(items, 5, false) + + // MustGetPageByPath uses the *entire* path (including query parts: ?page=2, etc.) + page := p.MustGetPageByPath(fullPath) + + // Start building the output (plain text or markdown). + out := "# AVL Pager + Render paths\n\n" + out += `This realm showcases how to maintain a paginated list while properly parsing render paths. +You can see how a single page can include a paginated element (like the example below), and how clicking +an item can take you to a dedicated page for that specific item. + +No matter how you browse through the paginated list, the introductory text (this section) remains the same. + +` + + out += ufmt.Sprintf("Showing page %d of %d\n\n", page.PageNumber, page.TotalPages) + + // List items for this page. + for _, item := range page.Items { + // Link each item to a details page: e.g. ":Config01" + out += ufmt.Sprintf("- [Item %s](/r/docs/avl_pager_params:%s)\n", item.Key, item.Key) + } + + // Insert pagination controls (previous/next links, etc.). + out += "\n" + page.Picker() + "\n\n" + out += "### [Go back to r/docs](/r/docs)" + + return out +} + +// renderConfigItem shows details for a single item, e.g. ":item001". +func renderConfigItem(itemName string) string { + value, ok := items.Get(itemName) + if !ok { + return ufmt.Sprintf("**No item found** for key: %s", itemName) + } + + out := ufmt.Sprintf("# Item %s\n\n%s\n\n", itemName, value.(string)) + out += "[Go back](/r/docs/avl_pager_params)" + return out +} diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index 28bac4171b5..be9a58e1c53 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -13,6 +13,7 @@ Explore various examples to learn more about Gno functionality and usage. - [Source](/r/docs/source) - View realm source code. - [Buttons](/r/docs/buttons) - Add buttons to your realm's render. - [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. +- [AVL Pager + Render paths](/r/docs/avl_pager_params) - Handle render arguments with pagination. - [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. - ... From 4d0000e8e10b13934e18a11b1220c27fd607926f Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:07:26 +0100 Subject: [PATCH 04/60] feat(gnoweb): "No render" page/component (#3611) ## Description Defining a `Render()` function in realms is optional. Currently gnoweb presents an error if a realm that doesn't have a render func is requested. This should not be the case. This PR also adds a VM error, `RenderNotDeclared`, which is to be returned when `vm/qrender` is called on a realm which does not have a `Render()` function declared. I updated the status component to return the following in the aforementioned case: Screenshot 2025-01-25 at 16 30 55 Also adds another `r/docs` realm mentioning that a render function is optional in `r/`. --- examples/gno.land/r/docs/docs.gno | 1 + .../gno.land/r/docs/optional_render/gno.mod | 1 + .../docs/optional_render/optional_render.gno | 7 +++ gno.land/pkg/gnoweb/app_test.go | 1 + gno.land/pkg/gnoweb/components/view_status.go | 34 +++++++++++++-- .../pkg/gnoweb/components/views/status.html | 10 +++-- gno.land/pkg/gnoweb/handler.go | 18 +++++--- gno.land/pkg/gnoweb/handler_test.go | 43 ++++++++++++++++++- gno.land/pkg/gnoweb/webclient.go | 3 +- gno.land/pkg/gnoweb/webclient_html.go | 5 +++ gno.land/pkg/gnoweb/webclient_mock.go | 25 ++++++++++- gno.land/pkg/sdk/vm/errors.go | 2 + gno.land/pkg/sdk/vm/handler.go | 4 ++ gno.land/pkg/sdk/vm/package.go | 1 + 14 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 examples/gno.land/r/docs/optional_render/gno.mod create mode 100644 examples/gno.land/r/docs/optional_render/optional_render.gno diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno index be9a58e1c53..b4c78205c0a 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -15,6 +15,7 @@ Explore various examples to learn more about Gno functionality and usage. - [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. - [AVL Pager + Render paths](/r/docs/avl_pager_params) - Handle render arguments with pagination. - [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. +- [Optional Render](/r/docs/optional_render) - Render() is optional in realms. - ... diff --git a/examples/gno.land/r/docs/optional_render/gno.mod b/examples/gno.land/r/docs/optional_render/gno.mod new file mode 100644 index 00000000000..4c8162ca46d --- /dev/null +++ b/examples/gno.land/r/docs/optional_render/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/optional_render diff --git a/examples/gno.land/r/docs/optional_render/optional_render.gno b/examples/gno.land/r/docs/optional_render/optional_render.gno new file mode 100644 index 00000000000..77da30609b3 --- /dev/null +++ b/examples/gno.land/r/docs/optional_render/optional_render.gno @@ -0,0 +1,7 @@ +package optional_render + +func Info() string { + return `Having a Render() function in your realm is optional! +If you do decide to have a Render() function, it must have the following signature: +func Render(path string) string { ... }` +} diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index 6fb69c6d984..eb17ee4d0e9 100644 --- a/gno.land/pkg/gnoweb/app_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -47,6 +47,7 @@ func TestRoutes(t *testing.T) { {"/game-of-realms", found, "/contribute"}, {"/gor", found, "/contribute"}, {"/blog", found, "/r/gnoland/blog"}, + {"/r/docs/optional_render", http.StatusNoContent, "No Render"}, {"/r/not/found/", notFound, ""}, {"/404/not/found", notFound, ""}, {"/아스키문자가아닌경로", notFound, ""}, diff --git a/gno.land/pkg/gnoweb/components/view_status.go b/gno.land/pkg/gnoweb/components/view_status.go index 46f998c45cb..56477a4db0a 100644 --- a/gno.land/pkg/gnoweb/components/view_status.go +++ b/gno.land/pkg/gnoweb/components/view_status.go @@ -2,10 +2,38 @@ package components const StatusViewType ViewType = "status-view" +// StatusData holds the dynamic fields for the "status" template type StatusData struct { - Message string + Title string + Body string + ButtonURL string + ButtonText string } -func StatusComponent(message string) *View { - return NewTemplateView(StatusViewType, "status", StatusData{message}) +// StatusErrorComponent returns a view for error scenarios +func StatusErrorComponent(message string) *View { + return NewTemplateView( + StatusViewType, + "status", + StatusData{ + Title: "Error: " + message, + Body: "Something went wrong.", + ButtonURL: "/", + ButtonText: "Go Back Home", + }, + ) +} + +// StatusNoRenderComponent returns a view for non-error notifications +func StatusNoRenderComponent(pkgPath string) *View { + return NewTemplateView( + StatusViewType, + "status", + StatusData{ + Title: "No Render", + Body: "This realm does not implement a Render() function.", + ButtonURL: pkgPath + "$source", + ButtonText: "View Realm Source", + }, + ) } diff --git a/gno.land/pkg/gnoweb/components/views/status.html b/gno.land/pkg/gnoweb/components/views/status.html index ab068cbf7e4..f4533275789 100644 --- a/gno.land/pkg/gnoweb/components/views/status.html +++ b/gno.land/pkg/gnoweb/components/views/status.html @@ -1,8 +1,12 @@ {{ define "status" }}
gno land -

Error: {{ .Message }}

-

Something went wrong. Let’s find our way back!

- Go Back Home +

+ {{ .Title }} +

+

{{ .Body }}

+ + {{ .ButtonText }} +
{{ end }} diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index cdaaa63e1bc..822fd50fa1b 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -114,7 +114,7 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components gnourl, err := ParseGnoURL(r.URL) if err != nil { h.Logger.Warn("unable to parse url path", "path", r.URL.Path, "error", err) - return http.StatusNotFound, components.StatusComponent("invalid path") + return http.StatusNotFound, components.StatusErrorComponent("invalid path") } breadcrumb := generateBreadcrumbPaths(gnourl) @@ -130,7 +130,7 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components return h.GetPackageView(gnourl) default: h.Logger.Debug("invalid path: path is neither a pure package or a realm") - return http.StatusBadRequest, components.StatusComponent("invalid path") + return http.StatusBadRequest, components.StatusErrorComponent("invalid path") } } @@ -160,6 +160,10 @@ func (h *WebHandler) GetRealmView(gnourl *GnoURL) (int, *components.View) { meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs()) if err != nil { + if errors.Is(err, ErrRenderNotDeclared) { + return http.StatusNoContent, components.StatusNoRenderComponent(gnourl.Path) + } + h.Logger.Error("unable to render realm", "error", err, "path", gnourl.EncodeURL()) return GetClientErrorStatusPage(gnourl, err) } @@ -223,7 +227,7 @@ func (h *WebHandler) GetSourceView(gnourl *GnoURL) (int, *components.View) { if len(files) == 0 { h.Logger.Debug("no files available", "path", gnourl.Path) - return http.StatusOK, components.StatusComponent("no files available") + return http.StatusOK, components.StatusErrorComponent("no files available") } var fileName string @@ -266,7 +270,7 @@ func (h *WebHandler) GetDirectoryView(gnourl *GnoURL) (int, *components.View) { if len(files) == 0 { h.Logger.Debug("no files available", "path", gnourl.Path) - return http.StatusOK, components.StatusComponent("no files available") + return http.StatusOK, components.StatusErrorComponent("no files available") } return http.StatusOK, components.DirectoryView(components.DirData{ @@ -283,13 +287,13 @@ func GetClientErrorStatusPage(_ *GnoURL, err error) (int, *components.View) { switch { case errors.Is(err, ErrClientPathNotFound): - return http.StatusNotFound, components.StatusComponent(err.Error()) + return http.StatusNotFound, components.StatusErrorComponent(err.Error()) case errors.Is(err, ErrClientBadRequest): - return http.StatusInternalServerError, components.StatusComponent("bad request") + return http.StatusInternalServerError, components.StatusErrorComponent("bad request") case errors.Is(err, ErrClientResponse): fallthrough // XXX: for now fallback as internal error default: - return http.StatusInternalServerError, components.StatusComponent("internal error") + return http.StatusInternalServerError, components.StatusErrorComponent("internal error") } } diff --git a/gno.land/pkg/gnoweb/handler_test.go b/gno.land/pkg/gnoweb/handler_test.go index 624e3390a97..e85434a6f41 100644 --- a/gno.land/pkg/gnoweb/handler_test.go +++ b/gno.land/pkg/gnoweb/handler_test.go @@ -24,12 +24,13 @@ func (t *testingLogger) Write(b []byte) (n int, err error) { // TestWebHandler_Get tests the Get method of WebHandler using table-driven tests. func TestWebHandler_Get(t *testing.T) { + t.Parallel() // Set up a mock package with some files and functions mockPackage := &gnoweb.MockPackage{ Domain: "example.com", Path: "/r/mock/path", Files: map[string]string{ - "render.gno": `package main; func Render(path string) { return "one more time" }`, + "render.gno": `package main; func Render(path string) string { return "one more time" }`, "gno.mod": `module example.com/r/mock/path`, "LicEnse": `my super license`, }, @@ -37,6 +38,10 @@ func TestWebHandler_Get(t *testing.T) { {FuncName: "SuperRenderFunction", Params: []vm.NamedType{ {Name: "my_super_arg", Type: "string"}, }}, + { + FuncName: "Render", Params: []vm.NamedType{{Name: "path", Type: "string"}}, + Results: []vm.NamedType{{Name: "", Type: "string"}}, + }, }, } @@ -82,6 +87,7 @@ func TestWebHandler_Get(t *testing.T) { for _, tc := range cases { t.Run(strings.TrimPrefix(tc.Path, "/"), func(t *testing.T) { + t.Parallel() t.Logf("input: %+v", tc) // Initialize testing logger @@ -110,3 +116,38 @@ func TestWebHandler_Get(t *testing.T) { }) } } + +// TestWebHandler_NoRender checks if gnoweb displays the `No Render` page properly. +// This happens when the render being queried does not have a Render function declared. +func TestWebHandler_NoRender(t *testing.T) { + t.Parallel() + + mockPath := "/r/mock/path" + mockPackage := &gnoweb.MockPackage{ + Domain: "gno.land", + Path: "/r/mock/path", + Files: map[string]string{ + "render.gno": `package main; func init() {}`, + "gno.mod": `module gno.land/r/mock/path`, + }, + } + + webclient := gnoweb.NewMockWebClient(mockPackage) + config := gnoweb.WebHandlerConfig{ + WebClient: webclient, + } + + logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{})) + handler, err := gnoweb.NewWebHandler(logger, config) + require.NoError(t, err, "failed to create WebHandler") + + req, err := http.NewRequest(http.MethodGet, mockPath, nil) + require.NoError(t, err, "failed to create HTTP request") + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusNoContent, rr.Code, "unexpected status code") + assert.Containsf(t, rr.Body.String(), "", "rendered body should contain: %q", "No Render") + assert.Containsf(t, rr.Body.String(), "", "rendered body should contain: %q", "This realm does not implement a Render() function.") +} diff --git a/gno.land/pkg/gnoweb/webclient.go b/gno.land/pkg/gnoweb/webclient.go index de44303f352..1def3bc3812 100644 --- a/gno.land/pkg/gnoweb/webclient.go +++ b/gno.land/pkg/gnoweb/webclient.go @@ -10,6 +10,7 @@ import ( var ( ErrClientPathNotFound = errors.New("package not found") + ErrRenderNotDeclared = errors.New("render function not declared") ErrClientBadRequest = errors.New("bad request") ErrClientResponse = errors.New("node response error") ) @@ -23,7 +24,7 @@ type RealmMeta struct { Toc md.Toc } -// WebClient is an interface for interacting with package and node ressources. +// WebClient is an interface for interacting with package and node resources. type WebClient interface { // RenderRealm renders the content of a realm from a given path and // arguments into the giver `writer`. The method should ensures the rendered diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go index d856c6f87a0..c04a7f9e457 100644 --- a/gno.land/pkg/gnoweb/webclient_html.go +++ b/gno.land/pkg/gnoweb/webclient_html.go @@ -177,6 +177,7 @@ func (s *HTMLWebClient) RenderRealm(w io.Writer, pkgPath string, args string) (* pkgPath = strings.Trim(pkgPath, "/") data := fmt.Sprintf("%s/%s:%s", s.domain, pkgPath, args) + rawres, err := s.query(qpath, []byte(data)) if err != nil { return nil, err @@ -213,6 +214,10 @@ func (s *HTMLWebClient) query(qpath string, data []byte) ([]byte, error) { return nil, ErrClientPathNotFound } + if errors.Is(err, vm.NoRenderDeclError{}) { + return nil, ErrRenderNotDeclared + } + s.logger.Error("response error", "path", qpath, "log", qres.Response.Log) return nil, fmt.Errorf("%w: %s", ErrClientResponse, err.Error()) } diff --git a/gno.land/pkg/gnoweb/webclient_mock.go b/gno.land/pkg/gnoweb/webclient_mock.go index 451f5e237c3..8a037c181e0 100644 --- a/gno.land/pkg/gnoweb/webclient_mock.go +++ b/gno.land/pkg/gnoweb/webclient_mock.go @@ -31,13 +31,18 @@ func NewMockWebClient(pkgs ...*MockPackage) *MockWebClient { return &MockWebClient{Packages: mpkgs} } -// Render simulates rendering a package by writing its content to the writer. +// RenderRealm simulates rendering a package by writing its content to the writer. func (m *MockWebClient) RenderRealm(w io.Writer, path string, args string) (*RealmMeta, error) { pkg, exists := m.Packages[path] if !exists { return nil, ErrClientPathNotFound } + if !pkgHasRender(pkg) { + return nil, ErrRenderNotDeclared + } + + // Write to the realm render fmt.Fprintf(w, "[%s]%s:", pkg.Domain, pkg.Path) // Return a dummy RealmMeta for simplicity @@ -89,3 +94,21 @@ func (m *MockWebClient) Sources(path string) ([]string, error) { return fileNames, nil } + +func pkgHasRender(pkg *MockPackage) bool { + if len(pkg.Functions) == 0 { + return false + } + + for _, fn := range pkg.Functions { + if fn.FuncName == "Render" && + len(fn.Params) == 1 && + len(fn.Results) == 1 && + fn.Params[0].Type == "string" && + fn.Results[0].Type == "string" { + return true + } + } + + return false +} diff --git a/gno.land/pkg/sdk/vm/errors.go b/gno.land/pkg/sdk/vm/errors.go index c8d6da98970..208fb074f7e 100644 --- a/gno.land/pkg/sdk/vm/errors.go +++ b/gno.land/pkg/sdk/vm/errors.go @@ -16,6 +16,7 @@ func (abciError) AssertABCIError() {} // NOTE: these are meant to be used in conjunction with pkgs/errors. type ( InvalidPkgPathError struct{ abciError } + NoRenderDeclError struct{ abciError } PkgExistError struct{ abciError } InvalidStmtError struct{ abciError } InvalidExprError struct{ abciError } @@ -27,6 +28,7 @@ type ( ) func (e InvalidPkgPathError) Error() string { return "invalid package path" } +func (e NoRenderDeclError) Error() string { return "render function not declared" } func (e PkgExistError) Error() string { return "package already exists" } func (e InvalidStmtError) Error() string { return "invalid statement" } func (e InvalidExprError) Error() string { return "invalid expression" } diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index c484e07e887..5aebf1afe46 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -129,9 +129,13 @@ func (vh vmHandler) queryRender(ctx sdk.Context, req abci.RequestQuery) (res abc expr := fmt.Sprintf("Render(%q)", path) result, err := vh.vm.QueryEvalString(ctx, pkgPath, expr) if err != nil { + if strings.Contains(err.Error(), "Render not declared") { + err = NoRenderDeclError{} + } res = sdk.ABCIResponseQueryFromError(err) return } + res.Data = []byte(result) return } diff --git a/gno.land/pkg/sdk/vm/package.go b/gno.land/pkg/sdk/vm/package.go index 0359061ccea..95e97648dac 100644 --- a/gno.land/pkg/sdk/vm/package.go +++ b/gno.land/pkg/sdk/vm/package.go @@ -20,6 +20,7 @@ var Package = amino.RegisterPackage(amino.NewPackage( // errors InvalidPkgPathError{}, "InvalidPkgPathError", + NoRenderDeclError{}, "NoRenderDeclError", PkgExistError{}, "PkgExistError", InvalidStmtError{}, "InvalidStmtError", InvalidExprError{}, "InvalidExprError", From 533ae676090c26bc6c9efc81aec5322f10d05e90 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:51:50 +0100 Subject: [PATCH 05/60] fix(gnoweb): NoRender response & test (#3634) ## Description Changes the NoRender response to 200, since 204 does not allow for content body. Also fixes a test that didn't catch this. --- gno.land/pkg/gnoweb/app_test.go | 2 +- gno.land/pkg/gnoweb/handler.go | 2 +- gno.land/pkg/gnoweb/handler_test.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index eb17ee4d0e9..ce10cae12d5 100644 --- a/gno.land/pkg/gnoweb/app_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -47,7 +47,7 @@ func TestRoutes(t *testing.T) { {"/game-of-realms", found, "/contribute"}, {"/gor", found, "/contribute"}, {"/blog", found, "/r/gnoland/blog"}, - {"/r/docs/optional_render", http.StatusNoContent, "No Render"}, + {"/r/docs/optional_render", http.StatusOK, "No Render"}, {"/r/not/found/", notFound, ""}, {"/404/not/found", notFound, ""}, {"/아스키문자가아닌경로", notFound, ""}, diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index 822fd50fa1b..ac39f4ce0f9 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -161,7 +161,7 @@ func (h *WebHandler) GetRealmView(gnourl *GnoURL) (int, *components.View) { meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs()) if err != nil { if errors.Is(err, ErrRenderNotDeclared) { - return http.StatusNoContent, components.StatusNoRenderComponent(gnourl.Path) + return http.StatusOK, components.StatusNoRenderComponent(gnourl.Path) } h.Logger.Error("unable to render realm", "error", err, "path", gnourl.EncodeURL()) diff --git a/gno.land/pkg/gnoweb/handler_test.go b/gno.land/pkg/gnoweb/handler_test.go index e85434a6f41..8321ad24be2 100644 --- a/gno.land/pkg/gnoweb/handler_test.go +++ b/gno.land/pkg/gnoweb/handler_test.go @@ -147,7 +147,7 @@ func TestWebHandler_NoRender(t *testing.T) { rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) - assert.Equal(t, http.StatusNoContent, rr.Code, "unexpected status code") - assert.Containsf(t, rr.Body.String(), "", "rendered body should contain: %q", "No Render") - assert.Containsf(t, rr.Body.String(), "", "rendered body should contain: %q", "This realm does not implement a Render() function.") + assert.Equal(t, http.StatusOK, rr.Code, "unexpected status code") + expectedBody := "This realm does not implement a Render() function." + assert.Contains(t, rr.Body.String(), expectedBody, "rendered body should contain: %q", expectedBody) } From 15d119fbf21817bd667b9107966a10502e09f605 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:21:07 +0100 Subject: [PATCH 06/60] feat: optimize jitter factor calculation (#3629) --- tm2/pkg/p2p/switch.go | 90 ++++++++++++++++++++------------------ tm2/pkg/p2p/switch_test.go | 67 ++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 43 deletions(-) diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index 0dd087026dd..7d9e768dd4b 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -1,11 +1,12 @@ package p2p import ( + "bytes" "context" "crypto/rand" + "encoding/binary" "fmt" "math" - "math/big" "sync" "time" @@ -356,7 +357,7 @@ func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { type backoffItem struct { lastDialTime time.Time - attempts int + attempts uint } var ( @@ -482,65 +483,68 @@ func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { } } -// calculateBackoff calculates a backoff time, -// based on the number of attempts and range limits +// calculateBackoff calculates the backoff interval by exponentiating the base interval +// by the number of attempts. The returned interval is capped at maxInterval and has a +// jitter factor applied to it (+/- 10% of interval, max 10 sec). func calculateBackoff( - attempts int, - minTimeout time.Duration, - maxTimeout time.Duration, + attempts uint, + baseInterval time.Duration, + maxInterval time.Duration, ) time.Duration { - var ( - minTime = time.Second * 1 - maxTime = time.Second * 60 - multiplier = float64(2) // exponential + const ( + defaultBaseInterval = time.Second * 1 + defaultMaxInterval = time.Second * 60 ) - // Check the min limit - if minTimeout > 0 { - minTime = minTimeout + // Sanitize base interval parameter. + if baseInterval <= 0 { + baseInterval = defaultBaseInterval } - // Check the max limit - if maxTimeout > 0 { - maxTime = maxTimeout + // Sanitize max interval parameter. + if maxInterval <= 0 { + maxInterval = defaultMaxInterval } - // Sanity check the range - if minTime >= maxTime { - return maxTime + // Calculate the interval by exponentiating the base interval by the number of attempts. + interval := baseInterval << attempts + + // Cap the interval to the maximum interval. + if interval > maxInterval { + interval = maxInterval } - // Calculate the backoff duration - var ( - base = float64(minTime) - calculated = base * math.Pow(multiplier, float64(attempts)) - ) + // Below is the code to add a jitter factor to the interval. + // Read random bytes into an 8 bytes buffer (size of an int64). + var randBytes [8]byte + if _, err := rand.Read(randBytes[:]); err != nil { + return interval + } - // Attempt to calculate the jitter factor - n, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - if err == nil { - jitterFactor := float64(n.Int64()) / float64(math.MaxInt64) // range [0, 1] + // Convert the random bytes to an int64. + var randInt64 int64 + _ = binary.Read(bytes.NewReader(randBytes[:]), binary.NativeEndian, &randInt64) - calculated = jitterFactor*(calculated-base) + base - } + // Calculate the random jitter multiplier (float between -1 and 1). + jitterMultiplier := float64(randInt64) / float64(math.MaxInt64) - // Prevent overflow for int64 (duration) cast - if calculated > float64(math.MaxInt64) { - return maxTime - } + const ( + maxJitterDuration = 10 * time.Second + maxJitterPercentage = 10 // 10% + ) - duration := time.Duration(calculated) + // Calculate the maximum jitter based on interval percentage. + maxJitter := interval * maxJitterPercentage / 100 - // Clamp the duration within bounds - if duration < minTime { - return minTime + // Cap the maximum jitter to the maximum duration. + if maxJitter > maxJitterDuration { + maxJitter = maxJitterDuration } - if duration > maxTime { - return maxTime - } + // Calculate the jitter. + jitter := time.Duration(float64(maxJitter) * jitterMultiplier) - return duration + return interval + jitter } // DialPeers adds the peers to the dial queue for async dialing. diff --git a/tm2/pkg/p2p/switch_test.go b/tm2/pkg/p2p/switch_test.go index 19a5db2efa5..cf0a0c41bb5 100644 --- a/tm2/pkg/p2p/switch_test.go +++ b/tm2/pkg/p2p/switch_test.go @@ -823,3 +823,70 @@ func TestMultiplexSwitch_DialPeers(t *testing.T) { } }) } + +func TestCalculateBackoff(t *testing.T) { + t.Parallel() + + checkJitterRange := func(t *testing.T, expectedAbs, actual time.Duration) { + t.Helper() + require.LessOrEqual(t, actual, expectedAbs) + require.GreaterOrEqual(t, actual, expectedAbs*-1) + } + + // Test that the default jitter factor is 10% of the backoff duration. + t.Run("percentage jitter", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(0, time.Second, 10*time.Minute)-time.Second) + checkJitterRange(t, 200*time.Millisecond, calculateBackoff(1, time.Second, 10*time.Minute)-2*time.Second) + checkJitterRange(t, 400*time.Millisecond, calculateBackoff(2, time.Second, 10*time.Minute)-4*time.Second) + checkJitterRange(t, 800*time.Millisecond, calculateBackoff(3, time.Second, 10*time.Minute)-8*time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(4, time.Second, 10*time.Minute)-16*time.Second) + } + }) + + // Test that the jitter factor is capped at 10 sec. + t.Run("capped jitter", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 10*time.Second, calculateBackoff(7, time.Second, 10*time.Minute)-128*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(10, time.Second, 20*time.Minute)-1024*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(20, time.Second, 300*time.Hour)-1048576*time.Second) + } + }) + + // Test that the backoff interval is based on the baseInterval. + t.Run("base interval", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 4800*time.Millisecond, calculateBackoff(4, 3*time.Second, 10*time.Minute)-48*time.Second) + checkJitterRange(t, 8*time.Second, calculateBackoff(3, 10*time.Second, 10*time.Minute)-80*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(5, 3*time.Hour, 100*time.Hour)-96*time.Hour) + } + }) + + // Test that the backoff interval is capped at maxInterval +/- jitter factor. + t.Run("max interval", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(10, 10*time.Hour, time.Second)-time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(10, 10*time.Hour, 16*time.Second)-16*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(10, 10*time.Hour, 128*time.Second)-128*time.Second) + } + }) + + // Test parameters sanitization for base and max intervals. + t.Run("parameters sanitization", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(0, -10, -10)-time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(4, -10, -10)-16*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(7, -10, 10*time.Minute)-128*time.Second) + } + }) +} From b392287f0d2c8262b5a020cd045b79a16ccb41fd Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:53:57 +0100 Subject: [PATCH 07/60] feat: gno mod graph (#3588) Basic initial version compatible with `go mod graph` in terms of output, while allowing the specification of folders through an optional argument. - [x] implement - [x] tests - [x] share examples Depends on #3587 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- gnovm/cmd/gno/mod.go | 49 ++++++++++++++++++++++++++++++++++++++- gnovm/cmd/gno/mod_test.go | 28 ++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index f303908d8ee..e394684561f 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -34,7 +34,7 @@ func newModCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newModDownloadCmd(io), // edit - // graph + newModGraphCmd(io), newModInitCmd(), newModTidy(io), // vendor @@ -61,6 +61,21 @@ func newModDownloadCmd(io commands.IO) *commands.Command { ) } +func newModGraphCmd(io commands.IO) *commands.Command { + cfg := &modGraphCfg{} + return commands.NewCommand( + commands.Metadata{ + Name: "graph", + ShortUsage: "graph [path]", + ShortHelp: "print module requirement graph", + }, + cfg, + func(_ context.Context, args []string) error { + return execModGraph(cfg, args, io) + }, + ) +} + func newModInitCmd() *commands.Command { return commands.NewCommand( commands.Metadata{ @@ -144,6 +159,38 @@ func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { ) } +type modGraphCfg struct{} + +func (c *modGraphCfg) RegisterFlags(fs *flag.FlagSet) { + // /out std + // /out remote + // /out _test processing + // ... +} + +func execModGraph(cfg *modGraphCfg, args []string, io commands.IO) error { + // default to current directory if no args provided + if len(args) == 0 { + args = []string{"."} + } + if len(args) > 1 { + return flag.ErrHelp + } + + stdout := io.Out() + + pkgs, err := gnomod.ListPkgs(args[0]) + if err != nil { + return err + } + for _, pkg := range pkgs { + for _, dep := range pkg.Imports { + fmt.Fprintf(stdout, "%s %s\n", pkg.Name, dep) + } + } + return nil +} + func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { if len(args) > 0 { return flag.ErrHelp diff --git a/gnovm/cmd/gno/mod_test.go b/gnovm/cmd/gno/mod_test.go index afce25597cd..e6fdce50a86 100644 --- a/gnovm/cmd/gno/mod_test.go +++ b/gnovm/cmd/gno/mod_test.go @@ -210,6 +210,34 @@ func TestModApp(t *testing.T) { # gno.land/p/demo/avl valid.gno +`, + }, + + // test `gno mod graph` + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/minimalist_gnomod", + simulateExternalRepo: true, + stdoutShouldBe: ``, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/valid1", + simulateExternalRepo: true, + stdoutShouldBe: ``, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/valid2", + simulateExternalRepo: true, + stdoutShouldBe: `gno.land/p/integ/valid gno.land/p/demo/avl +`, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/require_remote_module", + simulateExternalRepo: true, + stdoutShouldBe: `gno.land/tests/importavl gno.land/p/demo/avl `, }, } From 57da32437daa07a76e9478b1704832c0a211cfd2 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 30 Jan 2025 17:51:43 +0100 Subject: [PATCH 08/60] chore: Trigger CI tests on changes to the main go.mod (#3648) Resolves https://github.com/gnolang/gno/issues/3312 After a dependabot commit, the dependabot-tidy workflow [runs `make tidy`](https://github.com/gnolang/gno/blob/b392287f0d2c8262b5a020cd045b79a16ccb41fd/.github/workflows/dependabot-tidy.yml#L33). This changes `go.mod` in the subfolders of various tools, but these do not trigger the main CI test workflows. However, note that `make tidy` often also changes the main `go.mod` file. In this case, and other cases, it makes sense that a change in the main `go.mod` file should trigger the testing workflows since a change to dependency versions could effect test results. This PR updates workflow yml files for the global tests to trigger on a change the main `go.mod` file. (Thanks to feedback from @zivkovicmilos.) Signed-off-by: Jeff Thompson --- .github/workflows/gnoland.yml | 3 +++ .github/workflows/gnovm.yml | 3 +++ .github/workflows/tm2.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index b02e7b364e6..c4bc26a45fc 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -14,6 +14,9 @@ on: # Changes to examples/ can create failures in gno.land, eg. txtars, # see: https://github.com/gnolang/gno/pull/3590 - examples/** + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 7a015b74e09..08b0b66c4e8 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -8,6 +8,9 @@ on: paths: - gnovm/** - tm2/** # GnoVM has a dependency on TM2 types + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index 757391eab8c..d2157eb8828 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -7,6 +7,9 @@ on: pull_request: paths: - tm2/** + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: From d3774cefc766b6e5a9b464e4974e484a876408e9 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <72015889+0xtekgrinder@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:04:56 -0500 Subject: [PATCH 09/60] feat: ownable2step realm (#3594) Addresses: #3520 Add a ownable variant realm to have a transfer of ownership with two functions where the new owner needs to accept ownership to be sure no mistake can be made. --------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../p/oxtekgrinder/ownable2step/errors.gno | 10 ++ .../p/oxtekgrinder/ownable2step/gno.mod | 1 + .../p/oxtekgrinder/ownable2step/ownable.gno | 98 +++++++++++ .../ownable2step/ownable_test.gno | 156 ++++++++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno create mode 100644 examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod create mode 100644 examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno create mode 100644 examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno new file mode 100644 index 00000000000..6d91c9eb24b --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno @@ -0,0 +1,10 @@ +package ownable2step + +import "errors" + +var ( + ErrNoPendingOwner = errors.New("ownable2step: no pending owner") + ErrUnauthorized = errors.New("ownable2step: caller is not owner") + ErrPendingUnauthorized = errors.New("ownable2step: caller is not pending owner") + ErrInvalidAddress = errors.New("ownable2step: new owner address is invalid") +) diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod b/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod new file mode 100644 index 00000000000..0132a03418c --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod @@ -0,0 +1 @@ +module gno.land/p/oxtekgrinder/ownable2step diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno new file mode 100644 index 00000000000..43afa1cd141 --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno @@ -0,0 +1,98 @@ +package ownable2step + +import ( + "std" +) + +const OwnershipTransferEvent = "OwnershipTransfer" + +// Ownable2Step is a two-step ownership transfer package +// It allows the current owner to set a new owner and the new owner will need to accept the ownership before it is transferred +type Ownable2Step struct { + owner std.Address + pendingOwner std.Address +} + +func New() *Ownable2Step { + return &Ownable2Step{ + owner: std.PrevRealm().Addr(), + pendingOwner: "", + } +} + +func NewWithAddress(addr std.Address) *Ownable2Step { + return &Ownable2Step{ + owner: addr, + pendingOwner: "", + } +} + +// TransferOwnership initiate the transfer of the ownership to a new address by setting the PendingOwner +func (o *Ownable2Step) TransferOwnership(newOwner std.Address) error { + if !o.CallerIsOwner() { + return ErrUnauthorized + } + if !newOwner.IsValid() { + return ErrInvalidAddress + } + + o.pendingOwner = newOwner + return nil +} + +// AcceptOwnership accepts the pending ownership transfer +func (o *Ownable2Step) AcceptOwnership() error { + if o.pendingOwner.String() == "" { + return ErrNoPendingOwner + } + if std.PrevRealm().Addr() != o.pendingOwner { + return ErrPendingUnauthorized + } + + o.owner = o.pendingOwner + o.pendingOwner = "" + + return nil +} + +// DropOwnership removes the owner, effectively disabling any owner-related actions +// Top-level usage: disables all only-owner actions/functions, +// Embedded usage: behaves like a burn functionality, removing the owner from the struct +func (o *Ownable2Step) DropOwnership() error { + if !o.CallerIsOwner() { + return ErrUnauthorized + } + + prevOwner := o.owner + o.owner = "" + + std.Emit( + OwnershipTransferEvent, + "from", prevOwner.String(), + "to", "", + ) + + return nil +} + +// Owner returns the owner address from Ownable +func (o *Ownable2Step) Owner() std.Address { + return o.owner +} + +// PendingOwner returns the pending owner address from Ownable2Step +func (o *Ownable2Step) PendingOwner() std.Address { + return o.pendingOwner +} + +// CallerIsOwner checks if the caller of the function is the Realm's owner +func (o *Ownable2Step) CallerIsOwner() bool { + return std.PrevRealm().Addr() == o.owner +} + +// AssertCallerIsOwner panics if the caller is not the owner +func (o *Ownable2Step) AssertCallerIsOwner() { + if std.PrevRealm().Addr() != o.owner { + panic(ErrUnauthorized) + } +} diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno new file mode 100644 index 00000000000..4cca03b6ef5 --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno @@ -0,0 +1,156 @@ +package ownable2step + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") +) + +func TestNew(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + got := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, got, alice) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestNewWithAddress(t *testing.T) { + o := NewWithAddress(alice) + + got := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, got, alice) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestInitiateTransferOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, owner, alice) + uassert.Equal(t, pendingOwner, bob) +} + +func TestTransferOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, owner, alice) + uassert.Equal(t, pendingOwner, bob) + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + err = o.AcceptOwnership() + urequire.NoError(t, err) + + owner = o.Owner() + pendingOwner = o.PendingOwner() + + uassert.Equal(t, owner, bob) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestCallerIsOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + unauthorizedCaller := bob + + std.TestSetRealm(std.NewUserRealm(unauthorizedCaller)) + std.TestSetOrigCaller(unauthorizedCaller) + + uassert.False(t, o.CallerIsOwner()) +} + +func TestDropOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.DropOwnership() + urequire.NoError(t, err, "DropOwnership failed") + + owner := o.Owner() + uassert.Empty(t, owner, "owner should be empty") +} + +// Errors + +func TestErrUnauthorized(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + uassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error()) + uassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error()) +} + +func TestErrInvalidAddress(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.TransferOwnership("") + uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) + + err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") + uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) +} + +func TestErrNoPendingOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrNoPendingOwner.Error()) +} + +func TestErrPendingUnauthorized(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + std.TestSetRealm(std.NewUserRealm(alice)) + + err = o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrPendingUnauthorized.Error()) +} From 5c8dcfd3c368b6451f6da09b19aa6a08e884ea80 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Fri, 31 Jan 2025 22:05:09 +0200 Subject: [PATCH 10/60] fix(tm2/pkg/bft/node): make Node.startRPC not leak listeners on any error (#3640) Once Node.startRPC encounters an error, previously it was discarding all the created listeners and leaking them unclosed. This change fixes that using Go's named return values. Fixes #3639 --- tm2/pkg/bft/node/node.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index c1afb2996fa..08f1b3c58f9 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -713,7 +713,17 @@ func (n *Node) configureRPC() { rpccore.SetConfig(*n.config.RPC) } -func (n *Node) startRPC() ([]net.Listener, error) { +func (n *Node) startRPC() (listeners []net.Listener, err error) { + defer func() { + if err != nil { + // Close all the created listeners on any error, instead of + // leaking them: https://github.com/gnolang/gno/issues/3639 + for _, ln := range listeners { + ln.Close() + } + } + }() + listenAddrs := splitAndTrimEmpty(n.config.RPC.ListenAddress, ",", " ") config := rpcserver.DefaultConfig() @@ -729,8 +739,8 @@ func (n *Node) startRPC() ([]net.Listener, error) { // we may expose the rpc over both a unix and tcp socket var rebuildAddresses bool - listeners := make([]net.Listener, len(listenAddrs)) - for i, listenAddr := range listenAddrs { + listeners = make([]net.Listener, 0, len(listenAddrs)) + for _, listenAddr := range listenAddrs { mux := http.NewServeMux() rpcLogger := n.Logger.With("module", "rpc-server") wmLogger := rpcLogger.With("protocol", "websocket") @@ -782,7 +792,7 @@ func (n *Node) startRPC() ([]net.Listener, error) { ) } - listeners[i] = listener + listeners = append(listeners, listener) } if rebuildAddresses { n.config.RPC.ListenAddress = joinListenerAddresses(listeners) From 7992a29f706f152dc2f3470399236e0a33859b1e Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Fri, 31 Jan 2025 22:24:29 +0200 Subject: [PATCH 11/60] feat(tm2/pkg/iavl): add FuzzIterateRange and modernize FuzzMutableTree (#3548) This change hooks MutableTree fuzzing to Go's native fuzzing that's more intelligent and coverage guided to mutate inputs instead of naive random program generation. While here also added FuzzIterateRange. Updates #3087 --- tm2/pkg/iavl/tree_fuzz_test.go | 229 ++++++++++++++++++++++++++++----- 1 file changed, 199 insertions(+), 30 deletions(-) diff --git a/tm2/pkg/iavl/tree_fuzz_test.go b/tm2/pkg/iavl/tree_fuzz_test.go index 08645414fbf..ba709cc9da2 100644 --- a/tm2/pkg/iavl/tree_fuzz_test.go +++ b/tm2/pkg/iavl/tree_fuzz_test.go @@ -1,8 +1,14 @@ package iavl import ( + "encoding/json" "fmt" + "io" + "io/fs" "math/rand" + "os" + "path/filepath" + "strings" "testing" "github.com/gnolang/gno/tm2/pkg/db/memdb" @@ -14,28 +20,36 @@ import ( // A program is a list of instructions. type program struct { - instructions []instruction + Instructions []instruction `json:"instructions"` } func (p *program) Execute(tree *MutableTree) (err error) { var errLine int defer func() { - if r := recover(); r != nil { - var str string - - for i, instr := range p.instructions { - prefix := " " - if i == errLine { - prefix = ">> " - } - str += prefix + instr.String() + "\n" + r := recover() + if r == nil { + return + } + + // These are simply input errors and shouldn't be reported as actual logical issues. + if containsAny(fmt.Sprint(r), "Unrecognized op:", "Attempt to store nil value at key") { + return + } + + var str string + + for i, instr := range p.Instructions { + prefix := " " + if i == errLine { + prefix = ">> " } - err = fmt.Errorf("Program panicked with: %s\n%s", r, str) + str += prefix + instr.String() + "\n" } + err = fmt.Errorf("Program panicked with: %s\n%s", r, str) }() - for i, instr := range p.instructions { + for i, instr := range p.Instructions { errLine = i instr.Execute(tree) } @@ -43,39 +57,39 @@ func (p *program) Execute(tree *MutableTree) (err error) { } func (p *program) addInstruction(i instruction) { - p.instructions = append(p.instructions, i) + p.Instructions = append(p.Instructions, i) } func (p *program) size() int { - return len(p.instructions) + return len(p.Instructions) } type instruction struct { - op string - k, v []byte - version int64 + Op string + K, V []byte + Version int64 } func (i instruction) Execute(tree *MutableTree) { - switch i.op { + switch i.Op { case "SET": - tree.Set(i.k, i.v) + tree.Set(i.K, i.V) case "REMOVE": - tree.Remove(i.k) + tree.Remove(i.K) case "SAVE": tree.SaveVersion() case "DELETE": - tree.DeleteVersion(i.version) + tree.DeleteVersion(i.Version) default: - panic("Unrecognized op: " + i.op) + panic("Unrecognized op: " + i.Op) } } func (i instruction) String() string { - if i.version > 0 { - return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.op, i.k, i.v, i.version) + if i.Version > 0 { + return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.Op, i.K, i.V, i.Version) } - return fmt.Sprintf("%-8s %-8s %-8s", i.op, i.k, i.v) + return fmt.Sprintf("%-8s %-8s %-8s", i.Op, i.K, i.V) } // Generate a random program of the given size. @@ -88,15 +102,15 @@ func genRandomProgram(size int) *program { switch rand.Int() % 7 { case 0, 1, 2: - p.addInstruction(instruction{op: "SET", k: k, v: v}) + p.addInstruction(instruction{Op: "SET", K: k, V: v}) case 3, 4: - p.addInstruction(instruction{op: "REMOVE", k: k}) + p.addInstruction(instruction{Op: "REMOVE", K: k}) case 5: - p.addInstruction(instruction{op: "SAVE", version: int64(nextVersion)}) + p.addInstruction(instruction{Op: "SAVE", Version: int64(nextVersion)}) nextVersion++ case 6: if rv := rand.Int() % nextVersion; rv < nextVersion && rv > 0 { - p.addInstruction(instruction{op: "DELETE", version: int64(rv)}) + p.addInstruction(instruction{Op: "DELETE", Version: int64(rv)}) } } } @@ -107,19 +121,174 @@ func genRandomProgram(size int) *program { func TestMutableTreeFuzz(t *testing.T) { t.Parallel() + runThenGenerateMutableTreeFuzzSeeds(t, false) +} + +var pathForMutableTreeProgramSeeds = filepath.Join("testdata", "corpora", "mutable_tree_programs") + +func runThenGenerateMutableTreeFuzzSeeds(tb testing.TB, writeSeedsToFileSystem bool) { + tb.Helper() + + if testing.Short() { + tb.Skip("Running in -short mode") + } + maxIterations := testFuzzIterations progsPerIteration := 100000 iterations := 0 + if writeSeedsToFileSystem { + if err := os.MkdirAll(pathForMutableTreeProgramSeeds, 0o755); err != nil { + tb.Fatal(err) + } + } + for size := 5; iterations < maxIterations; size++ { for i := 0; i < progsPerIteration/size; i++ { tree := NewMutableTree(memdb.NewMemDB(), 0) program := genRandomProgram(size) err := program.Execute(tree) if err != nil { - t.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), tree.String()) + tb.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), tree.String()) } iterations++ + + if !writeSeedsToFileSystem { + continue + } + + // Otherwise write them to the testdata/corpra directory. + programJSON, err := json.Marshal(program) + if err != nil { + tb.Fatal(err) + } + path := filepath.Join(pathForMutableTreeProgramSeeds, fmt.Sprintf("%d", i+1)) + if err := os.WriteFile(path, programJSON, 0o755); err != nil { + tb.Fatal(err) + } + } + } +} + +type treeRange struct { + Start []byte + End []byte + Forward bool +} + +var basicRecords = []struct { + key, value string +}{ + {"abc", "123"}, + {"low", "high"}, + {"fan", "456"}, + {"foo", "a"}, + {"foobaz", "c"}, + {"good", "bye"}, + {"foobang", "d"}, + {"foobar", "b"}, + {"food", "e"}, + {"foml", "f"}, +} + +// Allows hooking into Go's fuzzers and then for continuous fuzzing +// enriched with coverage guided mutations, instead of naive mutations. +func FuzzIterateRange(f *testing.F) { + if testing.Short() { + f.Skip("Skipping in -short mode") + } + + // 1. Add the seeds. + seeds := []*treeRange{ + {[]byte("foo"), []byte("goo"), true}, + {[]byte("aaa"), []byte("abb"), true}, + {nil, []byte("flap"), true}, + {[]byte("foob"), nil, true}, + {[]byte("very"), nil, true}, + {[]byte("very"), nil, false}, + {[]byte("fooba"), []byte("food"), true}, + {[]byte("fooba"), []byte("food"), false}, + {[]byte("g"), nil, false}, + } + for _, seed := range seeds { + blob, err := json.Marshal(seed) + if err != nil { + f.Fatal(err) + } + f.Add(blob) + } + + db := memdb.NewMemDB() + tree := NewMutableTree(db, 0) + for _, br := range basicRecords { + tree.Set([]byte(br.key), []byte(br.value)) + } + + var trav traverser + + // 2. Run the fuzzer. + f.Fuzz(func(t *testing.T, rangeJSON []byte) { + tr := new(treeRange) + if err := json.Unmarshal(rangeJSON, tr); err != nil { + return + } + + tree.IterateRange(tr.Start, tr.End, tr.Forward, trav.view) + }) +} + +func containsAny(s string, anyOf ...string) bool { + for _, q := range anyOf { + if strings.Contains(s, q) { + return true + } + } + return false +} + +func FuzzMutableTreeInstructions(f *testing.F) { + if testing.Short() { + f.Skip("Skipping in -short mode") + } + + // 0. Generate then add the seeds. + runThenGenerateMutableTreeFuzzSeeds(f, true) + + // 1. Add the seeds. + dir := os.DirFS("testdata") + err := fs.WalkDir(dir, ".", func(path string, de fs.DirEntry, err error) error { + if de.IsDir() { + return err + } + + ff, err := dir.Open(path) + if err != nil { + return err + } + defer ff.Close() + + blob, err := io.ReadAll(ff) + if err != nil { + return err } + f.Add(blob) + return nil + }) + if err != nil { + f.Fatal(err) } + + // 2. Run the fuzzer. + f.Fuzz(func(t *testing.T, programJSON []byte) { + program := new(program) + if err := json.Unmarshal(programJSON, program); err != nil { + return + } + + tree := NewMutableTree(memdb.NewMemDB(), 0) + err := program.Execute(tree) + if err != nil { + t.Fatal(err) + } + }) } From c24f69fdae397cb9c7e833bf9a2d74c95edd4a78 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Fri, 31 Jan 2025 22:37:43 +0200 Subject: [PATCH 12/60] test(tm2/pkg/cmap): add benchmarks to show true impact of contention (#3540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go standard library's sync.Map is touted as great for cases with high load and is commonly known knowledge but the benchmark that I am committing shows otherwise that for this library's usage, it is so much more expensive hence this benchmark will avoid someone committing sync.Map without seeing the true implications. ```shell $ benchstat map_w_mutex.txt stdlib_sync_map.txt name old time/op new time/op delta CMapConcurrentInsertsDeletesHas-8 1.72s ±11% 1.92s ± 3% +11.66% (p=0.000 n=10+9) CMapHas-8 109ns ± 9% 118ns ± 3% +8.26% (p=0.002 n=10+8) name old alloc/op new alloc/op delta CMapConcurrentInsertsDeletesHas-8 1.18GB ± 2% 3.21GB ± 3% +172.09% (p=0.000 n=10+10) CMapHas-8 16.0B ± 0% 16.0B ± 0% ~ (all equal) name old allocs/op new allocs/op delta CMapConcurrentInsertsDeletesHas-8 824k ± 0% 4433k ± 0% +437.89% (p=0.000 n=10+10) CMapHas-8 2.00 ± 0% 1.60 ±38% ~ (p=0.065 n=9+10) ``` Updates #3505 --- tm2/pkg/cmap/cmap_test.go | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tm2/pkg/cmap/cmap_test.go b/tm2/pkg/cmap/cmap_test.go index d9051ea18d6..ebeb601633d 100644 --- a/tm2/pkg/cmap/cmap_test.go +++ b/tm2/pkg/cmap/cmap_test.go @@ -2,7 +2,9 @@ package cmap import ( "fmt" + "runtime" "strings" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -56,6 +58,61 @@ func TestContains(t *testing.T) { assert.Nil(t, cmap.Get("key2")) } +var sink any = nil + +func BenchmarkCMapConcurrentInsertsDeletesHas(b *testing.B) { + cm := NewCMap() + keys := make([]string, 100000) + for i := range keys { + keys[i] = fmt.Sprintf("key%d", i) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var wg sync.WaitGroup + semaCh := make(chan bool) + nCPU := runtime.NumCPU() + for j := 0; j < nCPU; j++ { + wg.Add(1) + go func() { + defer wg.Done() + + // Make sure that all the goroutines run at the + // exact same time for true concurrent tests. + <-semaCh + + for i, key := range keys { + if (j+i)%2 == 0 { + cm.Has(key) + } else { + cm.Set(key, j) + } + _ = cm.Size() + if (i+1)%3 == 0 { + cm.Delete(key) + } + + if (i+1)%327 == 0 { + cm.Clear() + } + _ = cm.Size() + _ = cm.Keys() + } + _ = cm.Values() + }() + } + close(semaCh) + wg.Wait() + + sink = semaCh + } + + if sink == nil { + b.Fatal("Benchmark did not run!") + } + sink = nil +} + func BenchmarkCMapHas(b *testing.B) { m := NewCMap() for i := 0; i < 1000; i++ { From 627eab289076cae5834037ecaf4ea7e921c73a2d Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 2 Feb 2025 13:56:43 +0100 Subject: [PATCH 13/60] feat(examples/test): quality of life improvements (#3661) ## Description This PR does two things: 1. Replaces the word "Fail" from test function names with `NotSucceed` - allows for easier ctrl+f search of the output locally and in the CI when something fails during `examples/make test` 2. Disables stress tests for `p/demo/diff` & `p/demo/btree`, cutting the examples/ test time by ~80% (baseline M2 mbp): ``` // master > time make test make test 140.04s user 3.09s system 217% cpu 1:05.95 total // PR > time make test make test 27.74s user 1.42s system 157% cpu 18.529 total ``` --------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/btree/btree_test.gno | 171 +++++++++--------- examples/gno.land/p/demo/diff/diff_test.gno | 13 +- examples/gno.land/p/demo/json/node_test.gno | 16 +- .../gno.land/p/demo/simpledao/dao_test.gno | 2 +- 4 files changed, 103 insertions(+), 99 deletions(-) diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno index 871e8c25e1d..959fa7a4254 100644 --- a/examples/gno.land/p/demo/btree/btree_test.gno +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -523,91 +523,14 @@ func TestBTree(t *testing.T) { } } -func TestStress(t *testing.T) { - // Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3. - // Insert 1000 records into each tree, then search for each record. - // Delete half of the records, skipping every other one, then search for each record. - - for degree := 3; degree <= 12; degree += 3 { - t.Logf("Testing B-Tree of degree %d\n", degree) - tree := New(WithDegree(degree)) - - // Insert 1000 records - t.Logf("Inserting 1000 records\n") - for i := 0; i < 1000; i++ { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - tree.Insert(content) - } - - // Search for all records - for i := 0; i < 1000; i++ { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - val := tree.Get(content) - if val == nil { - t.Errorf("Expected key %v, but didn't find it", content.Key) - } - } - - // Delete half of the records - for i := 0; i < 1000; i += 2 { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - tree.Delete(content) - } - - // Search for all records - for i := 0; i < 1000; i++ { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - val := tree.Get(content) - if i%2 == 0 { - if val != nil { - t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, val.(Content).Key, val.(Content).Value) - } - } else { - if val == nil { - t.Errorf("Expected key %v, but didn't find it", content.Key) - } - } - } - } - - // Now create a very large tree, with 100000 records - // Then delete roughly one third of them, using a very basic random number generation scheme - // (implement it right here) to determine which records to delete. - // Print a few lines using Logf to let the user know what's happening. - - t.Logf("Testing B-Tree of degree 10 with 100000 records\n") - tree := New(WithDegree(10)) - - // Insert 100000 records - t.Logf("Inserting 100000 records\n") - for i := 0; i < 100000; i++ { - content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} - tree.Insert(content) - } - - // Implement a very basic random number generator - seed := 0 - random := func() int { - seed = (seed*1103515245 + 12345) & 0x7fffffff - return seed - } - - // Delete one third of the records - t.Logf("Deleting one third of the records\n") - for i := 0; i < 35000; i++ { - content := Content{Key: random() % 100000, Value: fmt.Sprintf("Value_%d", i)} - tree.Delete(content) - } -} - -// Write a test that populates a large B-Tree with 10000 records. +// Write a test that populates a large B-Tree with 1000 records. // It should then `Clone` the tree, make some changes to both the original and the clone, // And then clone the clone, and make some changes to all three trees, and then check that the changes are isolated // to the tree they were made in. - func TestBTreeCloneIsolation(t *testing.T) { - t.Logf("Creating B-Tree of degree 10 with 10000 records\n") - tree := genericSeeding(New(WithDegree(10)), 10000) + t.Logf("Creating B-Tree of degree 10 with 1000 records\n") + size := 1000 + tree := genericSeeding(New(WithDegree(10)), size) // Clone the tree t.Logf("Cloning the tree\n") @@ -615,7 +538,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // Make some changes to the original and the clone t.Logf("Making changes to the original and the clone\n") - for i := 0; i < 10000; i += 2 { + for i := 0; i < size; i += 2 { content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} tree.Delete(content) content = Content{Key: i + 1, Value: fmt.Sprintf("Value_%d", i+1)} @@ -628,7 +551,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // Make some changes to all three trees t.Logf("Making changes to all three trees\n") - for i := 0; i < 10000; i += 3 { + for i := 0; i < size; i += 3 { content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} tree.Delete(content) content = Content{Key: i, Value: fmt.Sprintf("Value_%d", i+1)} @@ -639,7 +562,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // Check that the changes are isolated to the tree they were made in t.Logf("Checking that the changes are isolated to the tree they were made in\n") - for i := 0; i < 10000; i++ { + for i := 0; i < size; i++ { content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} val := tree.Get(content) @@ -676,3 +599,83 @@ func TestBTreeCloneIsolation(t *testing.T) { } } } + +// -------------------- +// Stress tests. Disabled for testing performance + +//func TestStress(t *testing.T) { +// // Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3. +// // Insert 1000 records into each tree, then search for each record. +// // Delete half of the records, skipping every other one, then search for each record. +// +// for degree := 3; degree <= 12; degree += 3 { +// t.Logf("Testing B-Tree of degree %d\n", degree) +// tree := New(WithDegree(degree)) +// +// // Insert 1000 records +// t.Logf("Inserting 1000 records\n") +// for i := 0; i < 1000; i++ { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// tree.Insert(content) +// } +// +// // Search for all records +// for i := 0; i < 1000; i++ { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// val := tree.Get(content) +// if val == nil { +// t.Errorf("Expected key %v, but didn't find it", content.Key) +// } +// } +// +// // Delete half of the records +// for i := 0; i < 1000; i += 2 { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// tree.Delete(content) +// } +// +// // Search for all records +// for i := 0; i < 1000; i++ { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// val := tree.Get(content) +// if i%2 == 0 { +// if val != nil { +// t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, val.(Content).Key, val.(Content).Value) +// } +// } else { +// if val == nil { +// t.Errorf("Expected key %v, but didn't find it", content.Key) +// } +// } +// } +// } +// +// // Now create a very large tree, with 100000 records +// // Then delete roughly one third of them, using a very basic random number generation scheme +// // (implement it right here) to determine which records to delete. +// // Print a few lines using Logf to let the user know what's happening. +// +// t.Logf("Testing B-Tree of degree 10 with 100000 records\n") +// tree := New(WithDegree(10)) +// +// // Insert 100000 records +// t.Logf("Inserting 100000 records\n") +// for i := 0; i < 100000; i++ { +// content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} +// tree.Insert(content) +// } +// +// // Implement a very basic random number generator +// seed := 0 +// random := func() int { +// seed = (seed*1103515245 + 12345) & 0x7fffffff +// return seed +// } +// +// // Delete one third of the records +// t.Logf("Deleting one third of the records\n") +// for i := 0; i < 35000; i++ { +// content := Content{Key: random() % 100000, Value: fmt.Sprintf("Value_%d", i)} +// tree.Delete(content) +// } +//} diff --git a/examples/gno.land/p/demo/diff/diff_test.gno b/examples/gno.land/p/demo/diff/diff_test.gno index bbf4fcdf3e0..3993c91664a 100644 --- a/examples/gno.land/p/demo/diff/diff_test.gno +++ b/examples/gno.land/p/demo/diff/diff_test.gno @@ -162,12 +162,13 @@ func TestMyersDiff(t *testing.T) { new: strings.Repeat("b", 1000), expected: "[-" + strings.Repeat("a", 1000) + "][+" + strings.Repeat("b", 1000) + "]", }, - { - name: "Very long strings", - old: strings.Repeat("a", 10000) + "b" + strings.Repeat("a", 10000), - new: strings.Repeat("a", 10000) + "c" + strings.Repeat("a", 10000), - expected: strings.Repeat("a", 10000) + "[-b][+c]" + strings.Repeat("a", 10000), - }, + //{ // disabled for testing performance + // XXX: consider adding a flag to run such tests, not like `-short`, or switching to a `-bench`, maybe. + // name: "Very long strings", + // old: strings.Repeat("a", 10000) + "b" + strings.Repeat("a", 10000), + // new: strings.Repeat("a", 10000) + "c" + strings.Repeat("a", 10000), + // expected: strings.Repeat("a", 10000) + "[-b][+c]" + strings.Repeat("a", 10000), + //}, } for _, tc := range tests { diff --git a/examples/gno.land/p/demo/json/node_test.gno b/examples/gno.land/p/demo/json/node_test.gno index dbc82369f68..c364187ac86 100644 --- a/examples/gno.land/p/demo/json/node_test.gno +++ b/examples/gno.land/p/demo/json/node_test.gno @@ -285,7 +285,7 @@ func TestNode_GetBool(t *testing.T) { } } -func TestNode_GetBool_Fail(t *testing.T) { +func TestNode_GetBool_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"literally null node", NullNode("")}, @@ -357,7 +357,7 @@ func TestNode_GetNull(t *testing.T) { } } -func TestNode_GetNull_Fail(t *testing.T) { +func TestNode_GetNull_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"number node is null", NumberNode("", 42)}, @@ -435,7 +435,7 @@ func TestNode_GetNumeric_With_Unmarshal(t *testing.T) { } } -func TestNode_GetNumeric_Fail(t *testing.T) { +func TestNode_GetNumeric_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -467,7 +467,7 @@ func TestNode_GetString(t *testing.T) { } } -func TestNode_GetString_Fail(t *testing.T) { +func TestNode_GetString_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -577,7 +577,7 @@ func TestNode_GetArray(t *testing.T) { } } -func TestNode_GetArray_Fail(t *testing.T) { +func TestNode_GetArray_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -736,7 +736,7 @@ func TestNode_Index(t *testing.T) { } } -func TestNode_Index_Fail(t *testing.T) { +func TestNode_Index_NotSucceed(t *testing.T) { tests := []struct { name string node *Node @@ -854,7 +854,7 @@ func TestNode_GetKey(t *testing.T) { } } -func TestNode_GetKey_Fail(t *testing.T) { +func TestNode_GetKey_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -998,7 +998,7 @@ func TestNode_GetObject(t *testing.T) { } } -func TestNode_GetObject_Fail(t *testing.T) { +func TestNode_GetObject_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"get object from null node", NullNode("")}, diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 46251e24dad..275455d1479 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -752,7 +752,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { dao.ExecutionSuccessful, }, { - "execution failed", + "execution not succeeded", dao.ExecutionFailed, }, } From 01abd50bb5ec0348e8c02a94e66c43eba079def9 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:26:00 +0100 Subject: [PATCH 14/60] chore(examples): modify pausable (#3628) ## Description Prevously, the `pausable` object embedded the `ownable` object directly, and with the intended usage of both packages being the following, `ownable` functions were being duplicated: ```go var ( Ownable = ownable.NewWithAddress(std.Address("xyz")) Pausable = pausable.NewFromOwnable(Ownable) ) ``` This PR names the `ownable` inside `pausable` as a private field and exposes a getter in case someone needs it for some reason. It also removes the `New()` function, which doesn't make too much sense right now as the pausable needs to be paired with `ownable` - this might change later but for now this should be the way. --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/ownable/ownable.gno | 18 +++++-- .../gno.land/p/demo/ownable/ownable_test.gno | 48 +++++++++++++++++++ .../gno.land/p/demo/pausable/pausable.gno | 27 +++++------ .../p/demo/pausable/pausable_test.gno | 42 +++++++--------- 4 files changed, 91 insertions(+), 44 deletions(-) diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index f565e27c0f2..a8cb5ea95a7 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -65,18 +65,28 @@ func (o *Ownable) DropOwnership() error { } // Owner returns the owner address from Ownable -func (o Ownable) Owner() std.Address { +func (o *Ownable) Owner() std.Address { + if o == nil { + return std.Address("") + } return o.owner } // CallerIsOwner checks if the caller of the function is the Realm's owner -func (o Ownable) CallerIsOwner() bool { +func (o *Ownable) CallerIsOwner() bool { + if o == nil { + return false + } return std.PrevRealm().Addr() == o.owner } // AssertCallerIsOwner panics if the caller is not the owner -func (o Ownable) AssertCallerIsOwner() { - if std.PrevRealm().Addr() != o.owner { +func (o *Ownable) AssertCallerIsOwner() { + if o == nil { + panic(ErrUnauthorized) + } + caller := std.PrevRealm().Addr() + if caller != o.owner { panic(ErrUnauthorized) } } diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno index f58af9642c6..d8b7f9a8e3a 100644 --- a/examples/gno.land/p/demo/ownable/ownable_test.gno +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -93,3 +93,51 @@ func TestErrInvalidAddress(t *testing.T) { err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) } + +func TestAssertCallerIsOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + // Should not panic when caller is owner + o.AssertCallerIsOwner() + + // Should panic when caller is not owner + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + defer func() { + r := recover() + if r == nil { + t.Error("expected panic but got none") + } + if r != ErrUnauthorized { + t.Errorf("expected ErrUnauthorized but got %v", r) + } + }() + o.AssertCallerIsOwner() +} + +func TestNilReceiver(t *testing.T) { + var o *Ownable + + owner := o.Owner() + if owner != std.Address("") { + t.Errorf("expected empty address but got %v", owner) + } + + isOwner := o.CallerIsOwner() + uassert.False(t, isOwner) + + defer func() { + r := recover() + if r == nil { + t.Error("expected panic but got none") + } + if r != ErrUnauthorized { + t.Errorf("expected ErrUnauthorized but got %v", r) + } + }() + o.AssertCallerIsOwner() +} diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index e6a85771fa6..fa3962cab41 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -7,23 +7,15 @@ import ( ) type Pausable struct { - *ownable.Ownable + o *ownable.Ownable paused bool } -// New returns a new Pausable struct with non-paused state as default -func New() *Pausable { - return &Pausable{ - Ownable: ownable.New(), - paused: false, - } -} - // NewFromOwnable is the same as New, but with a pre-existing top-level ownable func NewFromOwnable(ownable *ownable.Ownable) *Pausable { return &Pausable{ - Ownable: ownable, - paused: false, + o: ownable, + paused: false, } } @@ -34,24 +26,29 @@ func (p Pausable) IsPaused() bool { // Pause sets the state of Pausable to true, meaning all pausable functions are paused func (p *Pausable) Pause() error { - if !p.CallerIsOwner() { + if !p.o.CallerIsOwner() { return ownable.ErrUnauthorized } p.paused = true - std.Emit("Paused", "account", p.Owner().String()) + std.Emit("Paused", "account", p.o.Owner().String()) return nil } // Unpause sets the state of Pausable to false, meaning all pausable functions are resumed func (p *Pausable) Unpause() error { - if !p.CallerIsOwner() { + if !p.o.CallerIsOwner() { return ownable.ErrUnauthorized } p.paused = false - std.Emit("Unpaused", "account", p.Owner().String()) + std.Emit("Unpaused", "account", p.o.Owner().String()) return nil } + +// Ownable returns the underlying ownable +func (p *Pausable) Ownable() *ownable.Ownable { + return p.o +} diff --git a/examples/gno.land/p/demo/pausable/pausable_test.gno b/examples/gno.land/p/demo/pausable/pausable_test.gno index c9557245bdf..47028cd85c8 100644 --- a/examples/gno.land/p/demo/pausable/pausable_test.gno +++ b/examples/gno.land/p/demo/pausable/pausable_test.gno @@ -5,57 +5,49 @@ import ( "testing" "gno.land/p/demo/ownable" + "gno.land/p/demo/uassert" "gno.land/p/demo/urequire" ) var ( - firstCaller = std.Address("g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de") - secondCaller = std.Address("g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa") + firstCaller = std.Address("g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de") + o = ownable.NewWithAddress(firstCaller) ) -func TestNew(t *testing.T) { - std.TestSetOrigCaller(firstCaller) - - result := New() - - urequire.False(t, result.paused, "Expected result to be unpaused") - urequire.Equal(t, firstCaller.String(), result.Owner().String()) -} - func TestNewFromOwnable(t *testing.T) { std.TestSetOrigCaller(firstCaller) - o := ownable.New() - std.TestSetOrigCaller(secondCaller) result := NewFromOwnable(o) - - urequire.Equal(t, firstCaller.String(), result.Owner().String()) + urequire.Equal(t, firstCaller.String(), result.Ownable().Owner().String()) } func TestSetUnpaused(t *testing.T) { std.TestSetOrigCaller(firstCaller) + result := NewFromOwnable(o) - result := New() result.Unpause() - - urequire.False(t, result.IsPaused(), "Expected result to be unpaused") + uassert.False(t, result.IsPaused(), "Expected result to be unpaused") } func TestSetPaused(t *testing.T) { std.TestSetOrigCaller(firstCaller) + result := NewFromOwnable(o) - result := New() result.Pause() - - urequire.True(t, result.IsPaused(), "Expected result to be paused") + uassert.True(t, result.IsPaused(), "Expected result to be paused") } func TestIsPaused(t *testing.T) { - std.TestSetOrigCaller(firstCaller) - - result := New() + result := NewFromOwnable(o) urequire.False(t, result.IsPaused(), "Expected result to be unpaused") + std.TestSetOrigCaller(firstCaller) result.Pause() - urequire.True(t, result.IsPaused(), "Expected result to be paused") + uassert.True(t, result.IsPaused(), "Expected result to be paused") +} + +func TestOwnable(t *testing.T) { + result := NewFromOwnable(o) + + uassert.Equal(t, result.Ownable().Owner(), o.Owner()) } From dc0b608d46ab41896e582c4620d000362fc65ff3 Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Mon, 3 Feb 2025 17:53:47 +0900 Subject: [PATCH 15/60] feat(gnoweb): enable strikethrough UI (#3670) --- gno.land/pkg/gnoweb/webclient_html.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go index c04a7f9e457..72b1b3f8b06 100644 --- a/gno.land/pkg/gnoweb/webclient_html.go +++ b/gno.land/pkg/gnoweb/webclient_html.go @@ -49,6 +49,7 @@ func NewDefaultHTMLWebClientConfig(client *client.RPCClient) *HTMLWebClientConfi markdown.NewHighlighting( markdown.WithFormatOptions(chromaOptions...), ), + extension.Strikethrough, extension.Table, ), } From 815cf51273a20cd7752c72d5903b02d373584a65 Mon Sep 17 00:00:00 2001 From: Stefan Nikolic Date: Mon, 3 Feb 2025 15:35:21 +0100 Subject: [PATCH 16/60] feat: add FOMO3D game implementation (#3344) # Description This PR introduces FOMO3D, a blockchain-based game that combines lottery and investment mechanics, implemented as a Gno realm. The game creates an engaging economic model where players compete to be the last key purchaser while earning dividends from subsequent purchases. ### Key Features - Players purchase keys using GNOT tokens - Each key purchase: - Extends the game timer - Increases key price by 1% - Makes buyer potential jackpot winner - Distributes dividends to existing key holders - Automatic prize distribution: - 47% to jackpot (winner) - 28% as dividends to key holders - 20% to next round's starting pot - 5% as a fee to the contract owner - Full test coverage ### Technical Implementation - Utilizes AVL tree for player data storage - Implements dividend distribution system - Includes comprehensive test suite - Features markdown-formatted render functions for game state visualization - Mints a unique FOMO3D NFT to the winner of each round ### How to Use 1. Start game with `StartGame()` 2. Purchase keys with `BuyKeys()` 3. Claim dividends with `ClaimDividends()` 4. View game status via render functions 5. Winner automatically receives jackpot when timer expires ### Testing All core functionalities are covered by unit tests including: - Full game flow - Key purchasing mechanics - Dividend distribution - Game ending conditions Inspired by the original Ethereum FOMO3D game but rebuilt for the Gno platform. ## Note The test checks will not pass until [gnolang/gno#3495](https://github.com/gnolang/gno/pull/3495) is merged. In case this PR is not approved, I will refactor the NFT feature accordingly. --------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Signed-off-by: Norman Meier Signed-off-by: Norman Signed-off-by: Norman Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: Alexis Colin Co-authored-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Mustapha <102119509+mous1985@users.noreply.github.com> Co-authored-by: Morgan Co-authored-by: Blake <104744707+r3v4s@users.noreply.github.com> Co-authored-by: n0izn0iz Co-authored-by: Norman --- examples/gno.land/r/stefann/fomo3d/errors.gno | 30 ++ examples/gno.land/r/stefann/fomo3d/events.gno | 94 +++++ examples/gno.land/r/stefann/fomo3d/fomo3d.gno | 358 ++++++++++++++++++ .../gno.land/r/stefann/fomo3d/fomo3d_test.gno | 294 ++++++++++++++ examples/gno.land/r/stefann/fomo3d/gno.mod | 1 + examples/gno.land/r/stefann/fomo3d/nft.gno | 88 +++++ examples/gno.land/r/stefann/fomo3d/render.gno | 138 +++++++ 7 files changed, 1003 insertions(+) create mode 100644 examples/gno.land/r/stefann/fomo3d/errors.gno create mode 100644 examples/gno.land/r/stefann/fomo3d/events.gno create mode 100644 examples/gno.land/r/stefann/fomo3d/fomo3d.gno create mode 100644 examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno create mode 100644 examples/gno.land/r/stefann/fomo3d/gno.mod create mode 100644 examples/gno.land/r/stefann/fomo3d/nft.gno create mode 100644 examples/gno.land/r/stefann/fomo3d/render.gno diff --git a/examples/gno.land/r/stefann/fomo3d/errors.gno b/examples/gno.land/r/stefann/fomo3d/errors.gno new file mode 100644 index 00000000000..df70ab08c55 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/errors.gno @@ -0,0 +1,30 @@ +package fomo3d + +import "errors" + +var ( + // Game state errors + ErrGameInProgress = errors.New("fomo3d: game already in progress") + ErrGameNotInProgress = errors.New("fomo3d: game not in progress") + ErrGameEnded = errors.New("fomo3d: game has ended") + ErrGameTimeExpired = errors.New("fomo3d: game time expired") + ErrNoKeysPurchased = errors.New("fomo3d: no keys purchased") + ErrPlayerNotInGame = errors.New("fomo3d: player is not in the game") + + // Payment errors + ErrInvalidPayment = errors.New("fomo3d: must send ugnot only") + ErrInsufficientPayment = errors.New("fomo3d: insufficient payment for key") + + // Dividend errors + ErrNoDividendsToClaim = errors.New("fomo3d: no dividends to claim") + + // Fee errors + ErrNoFeesToClaim = errors.New("fomo3d: no owner fees to claim") + + // Resolution errors + ErrInvalidAddressOrName = errors.New("fomo3d: invalid address or unregistered username") + + // NFT errors + ErrUnauthorizedMint = errors.New("fomo3d: only the Fomo3D game realm can mint winner NFTs") + ErrZeroAddress = errors.New("fomo3d: zero address") +) diff --git a/examples/gno.land/r/stefann/fomo3d/events.gno b/examples/gno.land/r/stefann/fomo3d/events.gno new file mode 100644 index 00000000000..ea404466955 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/events.gno @@ -0,0 +1,94 @@ +package fomo3d + +import ( + "std" + + "gno.land/p/demo/ufmt" +) + +// Event names +const ( + // Game events + GameStartedEvent = "GameStarted" + GameEndedEvent = "GameEnded" + KeysPurchasedEvent = "KeysPurchased" + + // Player events + DividendsClaimedEvent = "DividendsClaimed" + + // Admin events + OwnerFeeClaimedEvent = "OwnerFeeClaimed" +) + +// Event keys +const ( + // Common keys + EventRoundKey = "round" + EventAmountKey = "amount" + + // Game keys + EventStartBlockKey = "startBlock" + EventEndBlockKey = "endBlock" + EventStartingPotKey = "startingPot" + EventWinnerKey = "winner" + EventJackpotKey = "jackpot" + + // Player keys + EventBuyerKey = "buyer" + EventNumKeysKey = "numKeys" + EventPriceKey = "price" + EventJackpotShareKey = "jackpotShare" + EventDividendShareKey = "dividendShare" + EventClaimerKey = "claimer" + + // Admin keys + EventOwnerKey = "owner" + EventPreviousOwnerKey = "previousOwner" + EventNewOwnerKey = "newOwner" +) + +func emitGameStarted(round, startBlock, endBlock, startingPot int64) { + std.Emit( + GameStartedEvent, + EventRoundKey, ufmt.Sprintf("%d", round), + EventStartBlockKey, ufmt.Sprintf("%d", startBlock), + EventEndBlockKey, ufmt.Sprintf("%d", endBlock), + EventStartingPotKey, ufmt.Sprintf("%d", startingPot), + ) +} + +func emitGameEnded(round int64, winner std.Address, jackpot int64) { + std.Emit( + GameEndedEvent, + EventRoundKey, ufmt.Sprintf("%d", round), + EventWinnerKey, winner.String(), + EventJackpotKey, ufmt.Sprintf("%d", jackpot), + ) +} + +func emitKeysPurchased(buyer std.Address, numKeys, price, jackpotShare, dividendShare int64) { + std.Emit( + KeysPurchasedEvent, + EventBuyerKey, buyer.String(), + EventNumKeysKey, ufmt.Sprintf("%d", numKeys), + EventPriceKey, ufmt.Sprintf("%d", price), + EventJackpotShareKey, ufmt.Sprintf("%d", jackpotShare), + EventDividendShareKey, ufmt.Sprintf("%d", dividendShare), + ) +} + +func emitDividendsClaimed(claimer std.Address, amount int64) { + std.Emit( + DividendsClaimedEvent, + EventClaimerKey, claimer.String(), + EventAmountKey, ufmt.Sprintf("%d", amount), + ) +} + +func emitOwnerFeeClaimed(owner std.Address, amount int64) { + std.Emit( + OwnerFeeClaimedEvent, + EventOwnerKey, owner.String(), + EventAmountKey, ufmt.Sprintf("%d", amount), + ) +} diff --git a/examples/gno.land/r/stefann/fomo3d/fomo3d.gno b/examples/gno.land/r/stefann/fomo3d/fomo3d.gno new file mode 100644 index 00000000000..b2384ba07f4 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/fomo3d.gno @@ -0,0 +1,358 @@ +package fomo3d + +import ( + "std" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + + "gno.land/r/demo/users" + "gno.land/r/leon/hof" +) + +// FOMO3D (Fear Of Missing Out 3D) is a blockchain-based game that combines elements +// of a lottery and investment mechanics. Players purchase keys using GNOT tokens, +// where each key purchase: +// - Extends the game timer +// - Increases the key price by 1% +// - Makes the buyer the potential winner of the jackpot +// - Distributes dividends to all key holders +// +// Game Mechanics: +// - The last person to buy a key before the timer expires wins the jackpot (47% of all purchases) +// - Key holders earn dividends from each purchase (28% of all purchases) +// - 20% of purchases go to the next round's starting pot +// - 5% goes to development fee +// - Game ends when the timer expires +// +// Inspired by the original Ethereum FOMO3D game but implemented in Gno. + +const ( + MIN_KEY_PRICE int64 = 100000 // minimum key price in ugnot + TIME_EXTENSION int64 = 86400 // time extension in blocks when new key is bought (~24 hours @ 1s blocks) + + // Distribution percentages (total 100%) + JACKPOT_PERCENT int64 = 47 // 47% goes to jackpot + DIVIDENDS_PERCENT int64 = 28 // 28% distributed to key holders + NEXT_ROUND_POT int64 = 20 // 20% goes to next round's starting pot + OWNER_FEE_PERCENT int64 = 5 // 5% goes to contract owner +) + +type PlayerInfo struct { + Keys int64 // number of keys owned + Dividends int64 // unclaimed dividends in ugnot +} + +// GameState represents the current state of the FOMO3D game +type GameState struct { // TODO: Separate GameState and RoundState and save round history tree in GameState + StartBlock int64 // Block when the game started + EndBlock int64 // Block when the game will end + LastKeyBlock int64 // Block of last key purchase + LastBuyer std.Address // Address of last key buyer + Jackpot int64 // Current jackpot in ugnot + KeyPrice int64 // Current price of keys in ugnot + TotalKeys int64 // Total number of keys in circulation + Ended bool // Whether the game has ended + CurrentRound int64 // Current round number + NextPot int64 // Next round's starting pot + OwnerFee int64 // Accumulated owner fees + BuyKeysLink string // Link to BuyKeys function + ClaimDividendsLink string // Link to ClaimDividends function + StartGameLink string // Link to StartGame function +} + +var ( + gameState GameState + players *avl.Tree // maps address -> PlayerInfo + Ownable *ownable.Ownable +) + +func init() { + Ownable = ownable.New() + players = avl.NewTree() + gameState.Ended = true + hof.Register() +} + +// StartGame starts a new game round +func StartGame() { + if !gameState.Ended && gameState.StartBlock != 0 { + panic(ErrGameInProgress.Error()) + } + + gameState.CurrentRound++ + gameState.StartBlock = std.GetHeight() + gameState.EndBlock = gameState.StartBlock + TIME_EXTENSION // Initial 24h window + gameState.LastKeyBlock = gameState.StartBlock + gameState.Jackpot = gameState.NextPot + gameState.NextPot = 0 + gameState.Ended = false + gameState.KeyPrice = MIN_KEY_PRICE + gameState.TotalKeys = 0 + + // Clear previous round's player data + players = avl.NewTree() + + emitGameStarted( + gameState.CurrentRound, + gameState.StartBlock, + gameState.EndBlock, + gameState.Jackpot, + ) +} + +// BuyKeys allows players to purchase keys +func BuyKeys() { + if gameState.Ended { + panic(ErrGameEnded.Error()) + } + + currentBlock := std.GetHeight() + if currentBlock > gameState.EndBlock { + panic(ErrGameTimeExpired.Error()) + } + + // Get sent coins + sent := std.GetOrigSend() + if len(sent) != 1 || sent[0].Denom != "ugnot" { + panic(ErrInvalidPayment.Error()) + } + + payment := sent.AmountOf("ugnot") + if payment < gameState.KeyPrice { + panic(ErrInsufficientPayment.Error()) + } + + // Calculate number of keys that can be bought and actual cost + numKeys := payment / gameState.KeyPrice + actualCost := numKeys * gameState.KeyPrice + excess := payment - actualCost + + // Update buyer's info + buyer := std.PrevRealm().Addr() + var buyerInfo PlayerInfo + if info, exists := players.Get(buyer.String()); exists { + buyerInfo = info.(PlayerInfo) + } + + buyerInfo.Keys += numKeys + gameState.TotalKeys += numKeys + + // Distribute actual cost + jackpotShare := actualCost * JACKPOT_PERCENT / 100 + dividendShare := actualCost * DIVIDENDS_PERCENT / 100 + nextPotShare := actualCost * NEXT_ROUND_POT / 100 + ownerShare := actualCost * OWNER_FEE_PERCENT / 100 + + // Update pools + gameState.Jackpot += jackpotShare + gameState.NextPot += nextPotShare + gameState.OwnerFee += ownerShare + + // Return excess payment to buyer if any + if excess > 0 { + banker := std.GetBanker(std.BankerTypeOrigSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + buyer, + std.NewCoins(std.NewCoin("ugnot", excess)), + ) + } + + // Distribute dividends to all key holders + if players.Size() > 0 && gameState.TotalKeys > 0 { + dividendPerKey := dividendShare / gameState.TotalKeys + players.Iterate("", "", func(key string, value interface{}) bool { + playerInfo := value.(PlayerInfo) + playerInfo.Dividends += playerInfo.Keys * dividendPerKey + players.Set(key, playerInfo) + return false + }) + } + + // Update game state + gameState.LastBuyer = buyer + gameState.LastKeyBlock = currentBlock + gameState.EndBlock = currentBlock + TIME_EXTENSION // Always extend 24h from current block + gameState.KeyPrice += (gameState.KeyPrice * numKeys) / 100 + + // Save buyer's updated info + players.Set(buyer.String(), buyerInfo) + + emitKeysPurchased( + buyer, + numKeys, + gameState.KeyPrice, + jackpotShare, + dividendShare, + ) +} + +// ClaimDividends allows players to withdraw their earned dividends +func ClaimDividends() { + caller := std.PrevRealm().Addr() + + info, exists := players.Get(caller.String()) + if !exists { + panic(ErrNoDividendsToClaim.Error()) + } + + playerInfo := info.(PlayerInfo) + if playerInfo.Dividends == 0 { + panic(ErrNoDividendsToClaim.Error()) + } + + // Reset dividends and send coins + amount := playerInfo.Dividends + playerInfo.Dividends = 0 + players.Set(caller.String(), playerInfo) + + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + caller, + std.NewCoins(std.NewCoin("ugnot", amount)), + ) + + emitDividendsClaimed(caller, amount) +} + +// ClaimOwnerFee allows the owner to withdraw accumulated fees +func ClaimOwnerFee() { + Ownable.AssertCallerIsOwner() + + if gameState.OwnerFee == 0 { + panic(ErrNoFeesToClaim.Error()) + } + + amount := gameState.OwnerFee + gameState.OwnerFee = 0 + + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + Ownable.Owner(), + std.NewCoins(std.NewCoin("ugnot", amount)), + ) + + emitOwnerFeeClaimed(Ownable.Owner(), amount) +} + +// EndGame ends the current round and distributes the jackpot +func EndGame() { + if gameState.Ended { + panic(ErrGameEnded.Error()) + } + + currentBlock := std.GetHeight() + if currentBlock <= gameState.EndBlock { + panic(ErrGameNotInProgress.Error()) + } + + if gameState.LastBuyer == "" { + panic(ErrNoKeysPurchased.Error()) + } + + gameState.Ended = true + + // Send jackpot to winner + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + gameState.LastBuyer, + std.NewCoins(std.NewCoin("ugnot", gameState.Jackpot)), + ) + + emitGameEnded( + gameState.CurrentRound, + gameState.LastBuyer, + gameState.Jackpot, + ) + + // Mint NFT for the winner + if err := mintRoundWinnerNFT(gameState.LastBuyer, gameState.CurrentRound); err != nil { + panic(err.Error()) + } +} + +// GetGameState returns current game state +func GetGameState() (int64, int64, int64, std.Address, int64, int64, int64, bool, int64, int64) { + return gameState.StartBlock, + gameState.EndBlock, + gameState.LastKeyBlock, + gameState.LastBuyer, + gameState.Jackpot, + gameState.KeyPrice, + gameState.TotalKeys, + gameState.Ended, + gameState.NextPot, + gameState.CurrentRound +} + +// GetOwnerInfo returns the owner address and unclaimed fees +func GetOwnerInfo() (std.Address, int64) { + return Ownable.Owner(), gameState.OwnerFee +} + +// Helper to convert string (address or username) to address +func stringToAddress(input string) std.Address { + // Check if input is valid address + addr := std.Address(input) + if addr.IsValid() { + return addr + } + + // Not an address, try to find namespace + if user := users.GetUserByName(input); user != nil { + return user.Address + } + + return "" +} + +func isPlayerInGame(addr std.Address) bool { + _, exists := players.Get(addr.String()) + return exists +} + +// GetPlayerInfo returns a player's keys and dividends +func GetPlayerInfo(addrOrName string) (int64, int64) { + addr := stringToAddress(addrOrName) + + if addr == "" { + panic(ErrInvalidAddressOrName.Error()) + } + + if !isPlayerInGame(addr) { + panic(ErrPlayerNotInGame.Error()) + } + + info, _ := players.Get(addr.String()) + playerInfo := info.(PlayerInfo) + return playerInfo.Keys, playerInfo.Dividends +} + +// Render handles the rendering of game state +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return RenderHome() + case c == 2 && parts[0] == "player": + if gameState.Ended { + return ufmt.Sprintf("🔴 Game has not started yet.\n\n Call [`StartGame()`](%s) to start a new round.\n\n", gameState.StartGameLink) + } + addr := stringToAddress(parts[1]) + if addr == "" || !isPlayerInGame(addr) { + return "Address not found in game. You need to buy keys first to view your stats.\n\n" + } + keys, dividends := GetPlayerInfo(parts[1]) + return RenderPlayer(addr, keys, dividends) + default: + return "404: Invalid path\n\n" + } +} diff --git a/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno b/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno new file mode 100644 index 00000000000..29f2a9b07a9 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno @@ -0,0 +1,294 @@ +package fomo3d + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" +) + +// Reset game state +func setupTestGame(t *testing.T) { + gameState = GameState{ + StartBlock: 0, + EndBlock: 0, + LastKeyBlock: 0, + LastBuyer: "", + Jackpot: 0, + KeyPrice: MIN_KEY_PRICE, + TotalKeys: 0, + Ended: true, + CurrentRound: 0, + NextPot: 0, + OwnerFee: 0, + } + players = avl.NewTree() + Ownable = ownable.New() +} + +// Test ownership functionality +func TestOwnership(t *testing.T) { + owner := testutils.TestAddress("owner") + nonOwner := testutils.TestAddress("nonOwner") + + // Set up initial owner + std.TestSetOrigCaller(owner) + std.TestSetOrigPkgAddr(owner) + setupTestGame(t) + + // Transfer ownership to nonOwner first to test ownership functions + std.TestSetOrigCaller(owner) + urequire.NotPanics(t, func() { + Ownable.TransferOwnership(nonOwner) + }) + + // Test fee accumulation + StartGame() + payment := MIN_KEY_PRICE * 10 + std.TestSetOrigCaller(owner) + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(owner, std.Coins{{"ugnot", payment}}) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + BuyKeys() + + // Verify fee accumulation + _, fees := GetOwnerInfo() + expectedFees := payment * OWNER_FEE_PERCENT / 100 + urequire.Equal(t, expectedFees, fees) + + // Test unauthorized fee claim (using old owner) + std.TestSetOrigCaller(owner) + urequire.PanicsWithMessage(t, "ownable: caller is not owner", ClaimOwnerFee) + + // Test authorized fee claim (using new owner) + std.TestSetOrigCaller(nonOwner) + initialBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(nonOwner) + std.TestIssueCoins(std.CurrentRealm().Addr(), std.Coins{{"ugnot", expectedFees}}) + urequire.NotPanics(t, ClaimOwnerFee) + + // Verify fees were claimed + _, feesAfter := GetOwnerInfo() + urequire.Equal(t, int64(0), feesAfter) + + finalBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(nonOwner) + urequire.Equal(t, initialBalance.AmountOf("ugnot")+expectedFees, finalBalance.AmountOf("ugnot")) +} + +// Test full game flow +func TestFullGameFlow(t *testing.T) { + setupTestGame(t) + + player1 := testutils.TestAddress("player1") + player2 := testutils.TestAddress("player2") + player3 := testutils.TestAddress("player3") + + // Test initial state + urequire.Equal(t, int64(0), gameState.CurrentRound) + urequire.Equal(t, MIN_KEY_PRICE, gameState.KeyPrice) + urequire.Equal(t, true, gameState.Ended) + + // Start game + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, std.GetHeight(), gameState.StartBlock) + urequire.Equal(t, int64(1), gameState.CurrentRound) + + t.Run("buying keys", func(t *testing.T) { + // Test insufficient payment + std.TestSetOrigCaller(player1) + std.TestIssueCoins(player1, std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + urequire.PanicsWithMessage(t, ErrInsufficientPayment.Error(), BuyKeys) + + // Test successful key purchase + payment := MIN_KEY_PRICE * 3 + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + + currentBlock := std.GetHeight() + urequire.NotPanics(t, BuyKeys) + + // Verify time extension + _, endBlock, _, _, _, _, _, _, _, _ := GetGameState() + urequire.Equal(t, currentBlock+TIME_EXTENSION, endBlock) + + // Verify player state + keys, dividends := GetPlayerInfo(player1.String()) + + urequire.Equal(t, int64(3), keys) + urequire.Equal(t, int64(0), dividends) + urequire.Equal(t, player1, gameState.LastBuyer) + + // Verify game state + _, endBlock, _, buyer, pot, price, keys, isEnded, nextPot, round := GetGameState() + urequire.Equal(t, player1, buyer) + urequire.Equal(t, int64(3), keys) + urequire.Equal(t, false, isEnded) + + urequire.Equal(t, payment*JACKPOT_PERCENT/100, pot) + + // Verify owner fee + _, ownerFees := GetOwnerInfo() + urequire.Equal(t, payment*OWNER_FEE_PERCENT/100, ownerFees) + }) + + t.Run("dividend distribution and claiming", func(t *testing.T) { + // Player 2 buys keys + std.TestSetOrigCaller(player2) + payment := gameState.KeyPrice * 2 // Buy 2 keys using current keyPrice + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + urequire.NotPanics(t, BuyKeys) + + // Check player1 received dividends + keys1, dividends1 := GetPlayerInfo(player1.String()) + + urequire.Equal(t, int64(3), keys1) + expectedDividends := payment * DIVIDENDS_PERCENT / 100 * 3 / gameState.TotalKeys + urequire.Equal(t, expectedDividends, dividends1) + + // Test claiming dividends + { + // Player1 claims dividends + std.TestSetOrigCaller(player1) + initialBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(player1) + urequire.NotPanics(t, ClaimDividends) + + // Verify dividends were claimed + _, dividendsAfter := GetPlayerInfo(player1.String()) + urequire.Equal(t, int64(0), dividendsAfter) + + lastBuyerBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(player1) + urequire.Equal(t, initialBalance.AmountOf("ugnot")+expectedDividends, lastBuyerBalance.AmountOf("ugnot")) + } + }) + + t.Run("game ending", func(t *testing.T) { + // Try ending too early + urequire.PanicsWithMessage(t, ErrGameNotInProgress.Error(), EndGame) + + // Skip to end of current time window + currentEndBlock := gameState.EndBlock + std.TestSkipHeights(currentEndBlock - std.GetHeight() + 1) + + // End game successfully + urequire.NotPanics(t, EndGame) + urequire.Equal(t, true, gameState.Ended) + urequire.Equal(t, int64(1), gameState.CurrentRound) + + // Verify winner received jackpot + lastBuyerBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(gameState.LastBuyer) + urequire.Equal(t, gameState.Jackpot, lastBuyerBalance.AmountOf("ugnot")) + + // Verify NFT was minted to winner + balance, err := BalanceOf(gameState.LastBuyer) + urequire.NoError(t, err) + urequire.Equal(t, uint64(1), balance) + + // Check NFT metadata + tokenID := grc721.TokenID("1") + metadata, err := TokenMetadata(tokenID) + + urequire.NoError(t, err) + urequire.Equal(t, "Fomo3D Winner - Round #1", metadata.Name) + }) + + // Test new round + t.Run("new round", func(t *testing.T) { + // Calculate expected next pot from previous round + payment1 := MIN_KEY_PRICE * 3 + // After buying 3 keys, price increased by 3% (1% per key) + secondKeyPrice := MIN_KEY_PRICE + (MIN_KEY_PRICE * 3 / 100) + payment2 := secondKeyPrice * 2 + expectedNextPot := (payment1 * NEXT_ROUND_POT / 100) + (payment2 * NEXT_ROUND_POT / 100) + + // Start new round + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, int64(2), gameState.CurrentRound) + + start, end, last, buyer, pot, price, keys, isEnded, nextPot, round := GetGameState() + urequire.Equal(t, int64(2), round) + urequire.Equal(t, expectedNextPot, pot) + urequire.Equal(t, int64(0), nextPot) + }) +} + +// Test individual components +func TestStartGame(t *testing.T) { + setupTestGame(t) + + // Test starting first game + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, std.GetHeight(), gameState.StartBlock) + + // Test cannot start while game in progress + urequire.PanicsWithMessage(t, ErrGameInProgress.Error(), StartGame) +} + +func TestBuyKeys(t *testing.T) { + setupTestGame(t) + StartGame() + + player := testutils.TestAddress("player") + std.TestSetOrigCaller(player) + + // Test invalid coin denomination + std.TestIssueCoins(player, std.Coins{{"invalid", MIN_KEY_PRICE}}) + std.TestSetOrigSend(std.Coins{{"invalid", MIN_KEY_PRICE}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"invalid", MIN_KEY_PRICE}}) + urequire.PanicsWithMessage(t, ErrInvalidPayment.Error(), BuyKeys) + + // Test multiple coin types + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}) + urequire.PanicsWithMessage(t, ErrInvalidPayment.Error(), BuyKeys) + + // Test insufficient payment + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + urequire.PanicsWithMessage(t, ErrInsufficientPayment.Error(), BuyKeys) + + // Test successful purchase + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + urequire.NotPanics(t, BuyKeys) +} + +func TestClaimDividends(t *testing.T) { + setupTestGame(t) + StartGame() + + player := testutils.TestAddress("player") + std.TestSetOrigCaller(player) + + // Test claiming with no dividends + urequire.PanicsWithMessage(t, ErrNoDividendsToClaim.Error(), ClaimDividends) + + // Setup player with dividends + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE}}) + BuyKeys() + + // Have another player buy to generate dividends + player2 := testutils.TestAddress("player2") + std.TestSetOrigCaller(player2) + std.TestIssueCoins(player2, std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + BuyKeys() + + // Test successful claim + std.TestSetOrigCaller(player) + urequire.NotPanics(t, ClaimDividends) +} diff --git a/examples/gno.land/r/stefann/fomo3d/gno.mod b/examples/gno.land/r/stefann/fomo3d/gno.mod new file mode 100644 index 00000000000..1b4e630a285 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/gno.mod @@ -0,0 +1 @@ +module gno.land/r/stefann/fomo3d diff --git a/examples/gno.land/r/stefann/fomo3d/nft.gno b/examples/gno.land/r/stefann/fomo3d/nft.gno new file mode 100644 index 00000000000..adea2fee795 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/nft.gno @@ -0,0 +1,88 @@ +package fomo3d + +import ( + "std" + "strconv" + + "gno.land/p/demo/grc/grc721" +) + +var ( + fomo3dNFT = grc721.NewNFTWithMetadata("Fomo3D Winner", "FOMO") +) + +// Public getters + +func Name() string { + return fomo3dNFT.Name() +} + +func Symbol() string { + return fomo3dNFT.Symbol() +} + +func BalanceOf(owner std.Address) (uint64, error) { + return fomo3dNFT.BalanceOf(owner) +} + +func OwnerOf(tokenID grc721.TokenID) (std.Address, error) { + return fomo3dNFT.OwnerOf(tokenID) +} + +func TokenMetadata(tokenID grc721.TokenID) (grc721.Metadata, error) { + return fomo3dNFT.TokenMetadata(tokenID) +} + +// Transfer and approval methods + +func TransferFrom(from, to std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.TransferFrom(from, to, tokenID) +} + +func SafeTransferFrom(from, to std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.SafeTransferFrom(from, to, tokenID) +} + +func Approve(approved std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.Approve(approved, tokenID) +} + +func GetApproved(tokenID grc721.TokenID) (std.Address, error) { + return fomo3dNFT.GetApproved(tokenID) +} + +func SetApprovalForAll(operator std.Address, approved bool) error { + return fomo3dNFT.SetApprovalForAll(operator, approved) +} + +func IsApprovedForAll(owner, operator std.Address) bool { + return fomo3dNFT.IsApprovedForAll(owner, operator) +} + +// Mints a new NFT for the round winner +func mintRoundWinnerNFT(winner std.Address, roundNumber int64) error { + if winner == "" { + return ErrZeroAddress + } + + roundStr := strconv.FormatInt(roundNumber, 10) + tokenID := grc721.TokenID(roundStr) + + // Create metadata + metadata := grc721.Metadata{ + Name: "Fomo3D Winner - Round #" + roundStr, + Description: "Winner of Fomo3D round #" + roundStr, + Image: "https://ipfs.io/ipfs/bafybeidayyli6bpewkhgtwqpgubmo77kmgjn4r5zq2i7usoyadcmvynhhq", + ExternalURL: "https://gno.land/r/stefann/fomo3d:round/" + roundStr, // TODO: Add this render in main realm that shows details of specific round + Attributes: []grc721.Trait{}, + BackgroundColor: "2D2D2D", // Dark theme background + } + + if err := fomo3dNFT.Mint(winner, tokenID); err != nil { + return err + } + + fomo3dNFT.SetTokenMetadata(tokenID, metadata) + + return nil +} diff --git a/examples/gno.land/r/stefann/fomo3d/render.gno b/examples/gno.land/r/stefann/fomo3d/render.gno new file mode 100644 index 00000000000..ba0c7b8f147 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/render.gno @@ -0,0 +1,138 @@ +package fomo3d + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ufmt" + + "gno.land/r/demo/users" +) + +// RenderHome renders the main game state +func RenderHome() string { + var builder strings.Builder + builder.WriteString("# FOMO3D - The Ultimate Game of Greed\n\n") + + // About section + builder.WriteString("## About the Game\n\n") + builder.WriteString("FOMO3D is a game that combines elements of lottery and investment mechanics. ") + builder.WriteString("Players purchase keys using GNOT tokens, where each key purchase:\n\n") + builder.WriteString("* Extends the game timer\n") + builder.WriteString("* Increases the key price by 1%\n") + builder.WriteString("* Makes you the potential winner of the jackpot\n") + builder.WriteString("* Distributes dividends to all key holders\n\n") + builder.WriteString("## How to Win\n\n") + builder.WriteString("* Be the last person to buy a key before the timer expires!\n\n") + builder.WriteString("**Rewards Distribution:**\n") + builder.WriteString("* 47% goes to the jackpot (for the winner)\n") + builder.WriteString("* 28% distributed as dividends to all key holders\n") + builder.WriteString("* 20% goes to next round's starting pot\n") + builder.WriteString("* 5% development fee for continuous improvement\n\n") + + // Play Game section + builder.WriteString("## How to Play\n\n") + builder.WriteString(ufmt.Sprintf("1. **Buy Keys** - Send GNOT to this realm with function [`BuyKeys()`](%s)\n", gameState.BuyKeysLink)) + builder.WriteString(ufmt.Sprintf("2. **Collect Dividends** - Call [`ClaimDividends()`](%s) to collect your earnings\n", gameState.ClaimDividendsLink)) + builder.WriteString("3. **Check Your Stats** - Append `:player/` followed by your address or namespace to the current URL to view your keys and dividends\n") + if gameState.Ended { + builder.WriteString(ufmt.Sprintf("4. **Start New Round** - Call [`StartGame()`](%s) to begin a new round\n", gameState.StartGameLink)) + } + builder.WriteString("\n") + + // Game Status section + builder.WriteString("## Game Status\n\n") + if gameState.StartBlock == 0 { + builder.WriteString("🔴 Game has not started yet.\n\n") + } else { + if gameState.Ended { + builder.WriteString("🔴 **Game Status:** Ended\n") + builder.WriteString(ufmt.Sprintf("🏆 **Winner:** %s\n\n", gameState.LastBuyer)) + } else { + builder.WriteString("🟢 **Game Status:** Active\n\n") + builder.WriteString(ufmt.Sprintf("🔄 **Round:** %d\n\n", gameState.CurrentRound)) + builder.WriteString(ufmt.Sprintf("⏱️ **Time Remaining:** %d blocks\n\n", gameState.EndBlock-std.GetHeight())) + } + builder.WriteString(ufmt.Sprintf("💰 **Jackpot:** %d ugnot\n\n", gameState.Jackpot)) + builder.WriteString(ufmt.Sprintf("🔑 **Key Price:** %d ugnot\n\n", gameState.KeyPrice)) + builder.WriteString(ufmt.Sprintf("📊 **Total Keys:** %d\n\n", gameState.TotalKeys)) + builder.WriteString(ufmt.Sprintf("👤 **Last Buyer:** %s\n\n", getDisplayName(gameState.LastBuyer))) + builder.WriteString(ufmt.Sprintf("🎮 **Next Round Pot:** %d ugnot\n\n", gameState.NextPot)) + } + + // Separator before less important sections + builder.WriteString("---\n\n") + + // Vote For Me section + builder.WriteString("### Vote For Us! 🗳️\n\n") + builder.WriteString("If you enjoy playing FOMO3D, please consider upvoting this game in the [Hall of Realms](https://gno.land/r/leon/hof)!\n\n") + builder.WriteString("Your support helps more players discover the game and grow our community! 🚀\n\n") + + // Report Bug section + builder.WriteString("### Report a Bug 🪲\n\n") + builder.WriteString("Something unusual happened? Help us improve the game by reporting bugs!\n") + builder.WriteString("[Visit our GitHub repository](https://github.com/gnolang/gno/issues)\n\n") + builder.WriteString("Please include:\n") + builder.WriteString("* Detailed description of what happened\n") + builder.WriteString("* Transaction hash (if applicable)\n") + builder.WriteString("* Your address\n") + builder.WriteString("* Current round number\n") + + return builder.String() +} + +// RenderPlayer renders specific player information +func RenderPlayer(addr std.Address, keys int64, dividends int64) string { + var builder strings.Builder + displayName := getDisplayName(addr) + builder.WriteString(ufmt.Sprintf("# Player Stats: %s\n\n", displayName)) + builder.WriteString("## Your Holdings\n\n") + builder.WriteString(ufmt.Sprintf("🔑 **Keys Owned:** %d\n\n", keys)) + builder.WriteString(ufmt.Sprintf("💰 **Unclaimed Dividends:** %d ugnot\n\n", dividends)) + + // Check if player has any NFTs + nftBalance, err := BalanceOf(addr) + if err == nil && nftBalance > 0 { + builder.WriteString("## Your Victory NFTs 🏆\n\n") + + // Iterate through all rounds up to current round to find player's NFTs + for i := int64(1); i <= gameState.CurrentRound; i++ { + tokenID := grc721.TokenID(strconv.FormatInt(i, 10)) + owner, err := OwnerOf(tokenID) + if err == nil && owner == addr { + metadata, err := TokenMetadata(tokenID) + if err == nil { + builder.WriteString(ufmt.Sprintf("### Round #%d Winner\n", i)) + builder.WriteString(ufmt.Sprintf("![NFT](%s)\n\n", metadata.Image)) + builder.WriteString("---\n\n") + } + } + } + } + + builder.WriteString("## Actions\n\n") + builder.WriteString(ufmt.Sprintf("* To buy more keys, send GNOT to this realm with [`BuyKeys()`](%s)\n", gameState.BuyKeysLink)) + if dividends > 0 { + builder.WriteString("* You have unclaimed dividends! Call `ClaimDividends()` to collect them\n") + } + + return builder.String() +} + +// Helper to get display name - just returns namespace if exists, otherwise address +func getDisplayName(addr std.Address) string { + if user := users.GetUserByAddress(addr); user != nil { + return user.Name + } + return addr.String() +} + +// UpdateFunctionLinks updates the links for game functions +func UpdateFunctionLinks(buyKeysLink string, claimDividendsLink string, startGameLink string) { + Ownable.AssertCallerIsOwner() + gameState.BuyKeysLink = buyKeysLink + gameState.ClaimDividendsLink = claimDividendsLink + gameState.StartGameLink = startGameLink +} From df14762147e9673381f629b3f5fcc79be4e70a58 Mon Sep 17 00:00:00 2001 From: Blake <104744707+r3v4s@users.noreply.github.com> Date: Tue, 4 Feb 2025 19:03:58 +0900 Subject: [PATCH 17/60] feat: bump max-gas same as current genesis (#3681) ## Description Default gas for genesis has been bumped in #3384 However we still have certain config uses old value. > env(or config) between actual and testing should be same as much as possible to avoid something like... this works with real gnoland but fail with in memory node. --- gno.land/pkg/gnoland/node_inmemory.go | 8 ++++---- gno.land/pkg/integration/node_testing.go | 8 ++++---- tm2/pkg/bft/types/params.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index cc9e74a78d8..3fd3063f8b9 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -60,10 +60,10 @@ func NewDefaultGenesisConfig(chainid, chaindomain string) *bft.GenesisDoc { func defaultBlockParams() *abci.BlockParams { return &abci.BlockParams{ - MaxTxBytes: 1_000_000, // 1MB, - MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 100_000_000, // 100M gas - TimeIotaMS: 100, // 100ms + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 3_000_000_000, // 3B gas + TimeIotaMS: 100, // 100ms } } diff --git a/gno.land/pkg/integration/node_testing.go b/gno.land/pkg/integration/node_testing.go index edcf53de5d3..c613048ebd7 100644 --- a/gno.land/pkg/integration/node_testing.go +++ b/gno.land/pkg/integration/node_testing.go @@ -103,10 +103,10 @@ func DefaultTestingGenesisConfig(gnoroot string, self crypto.PubKey, tmconfig *t ChainID: tmconfig.ChainID(), ConsensusParams: abci.ConsensusParams{ Block: &abci.BlockParams{ - MaxTxBytes: 1_000_000, // 1MB, - MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 100_000_000, // 100M gas - TimeIotaMS: 100, // 100ms + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 3_000_000_000, // 3B gas + TimeIotaMS: 100, // 100ms }, }, Validators: []bft.GenesisValidator{ diff --git a/tm2/pkg/bft/types/params.go b/tm2/pkg/bft/types/params.go index c2e8f304698..323e12c25cd 100644 --- a/tm2/pkg/bft/types/params.go +++ b/tm2/pkg/bft/types/params.go @@ -24,7 +24,7 @@ const ( MaxBlockDataBytes int64 = 2000000 // 2MB // MaxBlockMaxGas is the max gas limit for the block - MaxBlockMaxGas int64 = 100000000 // 100M gas + MaxBlockMaxGas int64 = 3000000000 // 3B gas // BlockTimeIotaMS is the block time iota (in ms) BlockTimeIotaMS int64 = 100 // ms From f8470906b4b57c552546049f100a2d90d7d348da Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:07:45 +0100 Subject: [PATCH 18/60] chore(examples): improve p/moul/txlink (#3682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Escape args properly. Related with #3668 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Jerónimo Albi --- .../p/moul/helplink/helplink_test.gno | 8 ++--- examples/gno.land/p/moul/txlink/txlink.gno | 33 ++++++++++--------- .../gno.land/p/moul/txlink/txlink_test.gno | 11 ++++--- .../gno.land/r/demo/boards/z_0_filetest.gno | 4 +-- .../r/demo/boards/z_10_c_filetest.gno | 6 ++-- .../gno.land/r/demo/boards/z_10_filetest.gno | 2 +- .../r/demo/boards/z_11_d_filetest.gno | 8 ++--- .../gno.land/r/demo/boards/z_11_filetest.gno | 4 +-- .../gno.land/r/demo/boards/z_12_filetest.gno | 2 +- .../gno.land/r/demo/boards/z_2_filetest.gno | 4 +-- .../gno.land/r/demo/boards/z_3_filetest.gno | 4 +-- .../gno.land/r/demo/boards/z_4_filetest.gno | 6 ++-- .../gno.land/r/demo/boards/z_5_c_filetest.gno | 4 +-- .../gno.land/r/demo/boards/z_5_filetest.gno | 6 ++-- .../gno.land/r/demo/boards/z_6_filetest.gno | 8 ++--- .../gno.land/r/demo/boards/z_7_filetest.gno | 2 +- .../gno.land/r/demo/boards/z_8_filetest.gno | 4 +-- .../gno.land/r/demo/boards/z_9_filetest.gno | 2 +- .../gno.land/r/docs/buttons/buttons_test.gno | 2 +- .../gno.land/r/leon/hof/datasource_test.gno | 4 +-- 20 files changed, 65 insertions(+), 59 deletions(-) diff --git a/examples/gno.land/p/moul/helplink/helplink_test.gno b/examples/gno.land/p/moul/helplink/helplink_test.gno index 29cfd02eb67..440001b94ce 100644 --- a/examples/gno.land/p/moul/helplink/helplink_test.gno +++ b/examples/gno.land/p/moul/helplink/helplink_test.gno @@ -18,7 +18,7 @@ func TestFunc(t *testing.T) { {"Realm Example", "foo", []string{"bar", "1", "baz", "2"}, "[Realm Example](/r/lorem/ipsum$help&func=foo&bar=1&baz=2)", "gno.land/r/lorem/ipsum"}, {"Single Arg", "testFunc", []string{"key", "value"}, "[Single Arg]($help&func=testFunc&key=value)", ""}, {"No Args", "noArgsFunc", []string{}, "[No Args]($help&func=noArgsFunc)", ""}, - {"Odd Args", "oddArgsFunc", []string{"key"}, "[Odd Args]($help&func=oddArgsFunc)", ""}, + {"Odd Args", "oddArgsFunc", []string{"key"}, "[Odd Args]($help&func=oddArgsFunc&error=odd+number+of+arguments)", ""}, } for _, tt := range tests { @@ -39,15 +39,15 @@ func TestFuncURL(t *testing.T) { {"foo", []string{"bar", "1", "baz", "2"}, "$help&func=foo&bar=1&baz=2", ""}, {"testFunc", []string{"key", "value"}, "$help&func=testFunc&key=value", ""}, {"noArgsFunc", []string{}, "$help&func=noArgsFunc", ""}, - {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc", ""}, + {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc&error=odd+number+of+arguments", ""}, {"foo", []string{"bar", "1", "baz", "2"}, "/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.land/r/lorem/ipsum"}, {"testFunc", []string{"key", "value"}, "/r/lorem/ipsum$help&func=testFunc&key=value", "gno.land/r/lorem/ipsum"}, {"noArgsFunc", []string{}, "/r/lorem/ipsum$help&func=noArgsFunc", "gno.land/r/lorem/ipsum"}, - {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc", "gno.land/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.land/r/lorem/ipsum"}, {"foo", []string{"bar", "1", "baz", "2"}, "https://gno.world/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.world/r/lorem/ipsum"}, {"testFunc", []string{"key", "value"}, "https://gno.world/r/lorem/ipsum$help&func=testFunc&key=value", "gno.world/r/lorem/ipsum"}, {"noArgsFunc", []string{}, "https://gno.world/r/lorem/ipsum$help&func=noArgsFunc", "gno.world/r/lorem/ipsum"}, - {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc", "gno.world/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.world/r/lorem/ipsum"}, } for _, tt := range tests { diff --git a/examples/gno.land/p/moul/txlink/txlink.gno b/examples/gno.land/p/moul/txlink/txlink.gno index 65edda6911e..8f753b4546d 100644 --- a/examples/gno.land/p/moul/txlink/txlink.gno +++ b/examples/gno.land/p/moul/txlink/txlink.gno @@ -15,6 +15,7 @@ package txlink import ( + "net/url" "std" "strings" ) @@ -51,24 +52,26 @@ func (r Realm) prefix() string { // Call returns a URL for the specified function with optional key-value // arguments. func (r Realm) Call(fn string, args ...string) string { - // Start with the base query - url := r.prefix() + "$help&func=" + fn + if len(args) == 0 { + return r.prefix() + "$help&func=" + fn + } + + // Create url.Values to properly encode parameters. + // But manage &func=fn as a special case to keep it as the first argument. + values := url.Values{} // Check if args length is even if len(args)%2 != 0 { - // If not even, we can choose to handle the error here. - // For example, we can just return the URL without appending - // more args. - return url - } - - // Append key-value pairs to the URL - for i := 0; i < len(args); i += 2 { - key := args[i] - value := args[i+1] - // XXX: escape keys and args - url += "&" + key + "=" + value + values.Add("error", "odd number of arguments") + } else { + // Add key-value pairs to values + for i := 0; i < len(args); i += 2 { + key := args[i] + value := args[i+1] + values.Add(key, value) + } } - return url + // Build the base URL and append encoded query parameters + return r.prefix() + "$help&func=" + fn + "&" + values.Encode() } diff --git a/examples/gno.land/p/moul/txlink/txlink_test.gno b/examples/gno.land/p/moul/txlink/txlink_test.gno index 61b532270d4..1da396b27a3 100644 --- a/examples/gno.land/p/moul/txlink/txlink_test.gno +++ b/examples/gno.land/p/moul/txlink/txlink_test.gno @@ -16,19 +16,22 @@ func TestCall(t *testing.T) { {"foo", []string{"bar", "1", "baz", "2"}, "$help&func=foo&bar=1&baz=2", ""}, {"testFunc", []string{"key", "value"}, "$help&func=testFunc&key=value", ""}, {"noArgsFunc", []string{}, "$help&func=noArgsFunc", ""}, - {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc", ""}, + {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc&error=odd+number+of+arguments", ""}, {"foo", []string{"bar", "1", "baz", "2"}, "/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.land/r/lorem/ipsum"}, {"testFunc", []string{"key", "value"}, "/r/lorem/ipsum$help&func=testFunc&key=value", "gno.land/r/lorem/ipsum"}, {"noArgsFunc", []string{}, "/r/lorem/ipsum$help&func=noArgsFunc", "gno.land/r/lorem/ipsum"}, - {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc", "gno.land/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.land/r/lorem/ipsum"}, {"foo", []string{"bar", "1", "baz", "2"}, "https://gno.world/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.world/r/lorem/ipsum"}, {"testFunc", []string{"key", "value"}, "https://gno.world/r/lorem/ipsum$help&func=testFunc&key=value", "gno.world/r/lorem/ipsum"}, {"noArgsFunc", []string{}, "https://gno.world/r/lorem/ipsum$help&func=noArgsFunc", "gno.world/r/lorem/ipsum"}, - {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc", "gno.world/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.world/r/lorem/ipsum"}, + {"test", []string{"key", "hello world"}, "$help&func=test&key=hello+world", ""}, + {"test", []string{"key", "a&b=c"}, "$help&func=test&key=a%26b%3Dc", ""}, + {"test", []string{"key", ""}, "$help&func=test&key=", ""}, } for _, tt := range tests { - title := tt.fn + title := string(tt.realm) + "_" + tt.fn t.Run(title, func(t *testing.T) { got := tt.realm.Call(tt.fn, tt.args...) urequire.Equal(t, tt.want, got) diff --git a/examples/gno.land/r/demo/boards/z_0_filetest.gno b/examples/gno.land/r/demo/boards/z_0_filetest.gno index a649895cb01..f56f6495b17 100644 --- a/examples/gno.land/r/demo/boards/z_0_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno @@ -30,12 +30,12 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] (0 replies) (0 reposts) // // ---------------------------------------- // ## [Second Post (title)](/r/demo/boards:test_board/2) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] (1 replies) (0 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno index 7dd460500d6..3fdd915a389 100644 --- a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno @@ -35,15 +35,15 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // > First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // // ---------------------------------------------------- // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_10_filetest.gno b/examples/gno.land/r/demo/boards/z_10_filetest.gno index 8a6d11c79cf..80254592d5f 100644 --- a/examples/gno.land/r/demo/boards/z_10_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_filetest.gno @@ -33,7 +33,7 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // ---------------------------------------------------- // thread does not exist with id: 1 diff --git a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno index f64b4c84bba..0a5c1886d24 100644 --- a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno @@ -35,19 +35,19 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // > First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // // ---------------------------------------------------- // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // > Edited: First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_11_filetest.gno b/examples/gno.land/r/demo/boards/z_11_filetest.gno index 3f56293b3bd..1f04d7b686d 100644 --- a/examples/gno.land/r/demo/boards/z_11_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno @@ -33,11 +33,11 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // ---------------------------------------------------- // # Edited: First Post in (title) // // Edited: Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_12_filetest.gno b/examples/gno.land/r/demo/boards/z_12_filetest.gno index ac4adf6ee7b..a362ea59823 100644 --- a/examples/gno.land/r/demo/boards/z_12_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_filetest.gno @@ -37,6 +37,6 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board1/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (1 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] (0 replies) (1 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_2_filetest.gno b/examples/gno.land/r/demo/boards/z_2_filetest.gno index 31b39644b24..55f94e10f1a 100644 --- a/examples/gno.land/r/demo/boards/z_2_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_2_filetest.gno @@ -32,8 +32,8 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_3_filetest.gno b/examples/gno.land/r/demo/boards/z_3_filetest.gno index 0b2a2df2f91..c2aa264b1d2 100644 --- a/examples/gno.land/r/demo/boards/z_3_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_3_filetest.gno @@ -34,8 +34,8 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_4_filetest.gno b/examples/gno.land/r/demo/boards/z_4_filetest.gno index b781e94e4db..aede35077f9 100644 --- a/examples/gno.land/r/demo/boards/z_4_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno @@ -37,13 +37,13 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // > Second reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // // Realm: diff --git a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno index 723e6a10204..176c11da73c 100644 --- a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno @@ -33,8 +33,8 @@ func main() { // # First Post (title) // // Body of the first post. (body) -// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // > Reply of the first post -// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_5_filetest.gno b/examples/gno.land/r/demo/boards/z_5_filetest.gno index 712af483891..4f6f5cb9b75 100644 --- a/examples/gno.land/r/demo/boards/z_5_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_filetest.gno @@ -33,12 +33,12 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // > Second reply of the second post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_6_filetest.gno b/examples/gno.land/r/demo/boards/z_6_filetest.gno index ec40cf5f8e9..39791606aae 100644 --- a/examples/gno.land/r/demo/boards/z_6_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_6_filetest.gno @@ -35,16 +35,16 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // > // > > First reply of the first reply // > > -// > > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] +// > > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=5&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=5&threadid=2)] // // > Second reply of the second post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_7_filetest.gno b/examples/gno.land/r/demo/boards/z_7_filetest.gno index 353b84f6d87..9ff95f7492d 100644 --- a/examples/gno.land/r/demo/boards/z_7_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno @@ -28,6 +28,6 @@ func main() { // ## [First Post (title)](/r/demo/boards:test_board/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] (0 replies) (0 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_8_filetest.gno b/examples/gno.land/r/demo/boards/z_8_filetest.gno index 4896dfcfccf..47d84737698 100644 --- a/examples/gno.land/r/demo/boards/z_8_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno @@ -35,11 +35,11 @@ func main() { // _[see thread](/r/demo/boards:test_board/2)_ // // Reply of the second post -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // _[see all 1 replies](/r/demo/boards:test_board/2/3)_ // // > First reply of the first reply // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&postid=5&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=5&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_9_filetest.gno b/examples/gno.land/r/demo/boards/z_9_filetest.gno index ca37e306bda..96af90263d5 100644 --- a/examples/gno.land/r/demo/boards/z_9_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_filetest.gno @@ -34,5 +34,5 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&threadid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&postid=1&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&postid=1&threadid=1)] // diff --git a/examples/gno.land/r/docs/buttons/buttons_test.gno b/examples/gno.land/r/docs/buttons/buttons_test.gno index 2903fa1a858..c6164f3c687 100644 --- a/examples/gno.land/r/docs/buttons/buttons_test.gno +++ b/examples/gno.land/r/docs/buttons/buttons_test.gno @@ -7,7 +7,7 @@ import ( func TestRenderMotdLink(t *testing.T) { res := Render("motd") - const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message%21" if !strings.Contains(res, wantLink) { t.Fatalf("%s\ndoes not contain correct help page link: %s", res, wantLink) } diff --git a/examples/gno.land/r/leon/hof/datasource_test.gno b/examples/gno.land/r/leon/hof/datasource_test.gno index 376f981875f..fb67f20e7e7 100644 --- a/examples/gno.land/r/leon/hof/datasource_test.gno +++ b/examples/gno.land/r/leon/hof/datasource_test.gno @@ -151,7 +151,7 @@ func TestItemRecord(t *testing.T) { content, _ := r.Content() wantContent := "# Submission #1\n\n\n```\ngno.land/r/demo/test\n```\n\nby demo\n\n" + "[View realm](/r/demo/test)\n\nSubmitted at Block #42\n\n" + - "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land/r/demo/test) - " + - "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land/r/demo/test)\n\n" + "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest) - " + + "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest)\n\n" uassert.Equal(t, wantContent, content) } From 85a8740bdafb790c556ef5ce485e08357bec54e5 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:16:00 +0100 Subject: [PATCH 19/60] feat(stdlibs/std)!: replace `IsOriginCall` with `PrevRealm().IsUser()` for EOA checks (#3399) Linked to https://github.com/gnolang/gno/issues/1475 --------- Co-authored-by: Morgan Bazalgette --- docs/reference/stdlibs/std/chain.md | 12 ------- .../gno.land/r/demo/banktest/z_3_filetest.gno | 1 - examples/gno.land/r/demo/boards/public.gno | 12 +++---- .../gno.land/r/demo/boards/z_0_a_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_0_b_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_0_c_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_0_d_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_0_e_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_0_filetest.gno | 5 +++ .../r/demo/boards/z_10_a_filetest.gno | 6 ++++ .../r/demo/boards/z_10_b_filetest.gno | 6 ++++ .../r/demo/boards/z_10_c_filetest.gno | 6 ++++ .../gno.land/r/demo/boards/z_10_filetest.gno | 6 ++++ .../r/demo/boards/z_11_a_filetest.gno | 6 ++++ .../r/demo/boards/z_11_b_filetest.gno | 6 ++++ .../r/demo/boards/z_11_c_filetest.gno | 6 ++++ .../r/demo/boards/z_11_d_filetest.gno | 6 ++++ .../gno.land/r/demo/boards/z_11_filetest.gno | 6 ++++ .../r/demo/boards/z_12_a_filetest.gno | 2 ++ .../r/demo/boards/z_12_b_filetest.gno | 5 +++ .../r/demo/boards/z_12_c_filetest.gno | 5 +++ .../r/demo/boards/z_12_d_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_12_filetest.gno | 7 +++++ .../gno.land/r/demo/boards/z_1_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_2_filetest.gno | 4 +++ .../gno.land/r/demo/boards/z_3_filetest.gno | 6 ++++ .../gno.land/r/demo/boards/z_4_filetest.gno | 6 ++++ .../gno.land/r/demo/boards/z_5_b_filetest.gno | 2 ++ .../gno.land/r/demo/boards/z_5_c_filetest.gno | 2 ++ .../gno.land/r/demo/boards/z_5_d_filetest.gno | 2 ++ .../gno.land/r/demo/boards/z_5_filetest.gno | 6 ++++ .../gno.land/r/demo/boards/z_6_filetest.gno | 6 ++++ .../gno.land/r/demo/boards/z_7_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_8_filetest.gno | 6 ++++ .../gno.land/r/demo/boards/z_9_a_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_9_b_filetest.gno | 5 +++ .../gno.land/r/demo/boards/z_9_filetest.gno | 4 +++ .../r/demo/tests/subtests/subtests.gno | 2 +- examples/gno.land/r/demo/tests/tests.gno | 2 +- examples/gno.land/r/demo/tests/tests_test.gno | 31 ++++++++++++------- .../gno.land/r/demo/users/z_11_filetest.gno | 2 +- .../gno.land/r/demo/users/z_11b_filetest.gno | 2 +- .../gno.land/r/demo/users/z_5_filetest.gno | 3 ++ .../gno.land/r/demo/users/z_6_filetest.gno | 2 +- .../transpile/valid_transpile_file.txtar | 4 +-- gnovm/pkg/transpiler/transpiler_test.go | 14 ++++----- gnovm/stdlibs/generated.go | 20 ------------ gnovm/stdlibs/std/native.gno | 12 +++---- gnovm/stdlibs/std/native.go | 4 +-- gnovm/stdlibs/std/native_test.go | 2 +- gnovm/tests/files/std5.gno | 4 +-- gnovm/tests/files/std8.gno | 4 +-- gnovm/tests/stdlibs/generated.go | 20 ------------ gnovm/tests/stdlibs/std/std.gno | 1 - gnovm/tests/stdlibs/std/std.go | 4 +-- 55 files changed, 227 insertions(+), 103 deletions(-) diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 6a1da6483fd..f3b2c6be0b8 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -4,18 +4,6 @@ id: chain # Chain-related -## IsOriginCall -```go -func IsOriginCall() bool -``` -Checks if the caller of the function is an EOA. Returns **true** if caller is an EOA, **false** otherwise. - -#### Usage -```go -if !std.IsOriginCall() {...} -``` ---- - ## AssertOriginCall ```go func AssertOriginCall() diff --git a/examples/gno.land/r/demo/banktest/z_3_filetest.gno b/examples/gno.land/r/demo/banktest/z_3_filetest.gno index 7b6758c3e4f..7bf2aea4f38 100644 --- a/examples/gno.land/r/demo/banktest/z_3_filetest.gno +++ b/examples/gno.land/r/demo/banktest/z_3_filetest.gno @@ -18,7 +18,6 @@ func main() { banker := std.GetBanker(std.BankerTypeRealmSend) send := std.Coins{{"ugnot", 123}} banker.SendCoins(banktestAddr, mainaddr, send) - } // Error: diff --git a/examples/gno.land/r/demo/boards/public.gno b/examples/gno.land/r/demo/boards/public.gno index 1d26126fcb2..db545446641 100644 --- a/examples/gno.land/r/demo/boards/public.gno +++ b/examples/gno.land/r/demo/boards/public.gno @@ -17,7 +17,7 @@ func GetBoardIDFromName(name string) (BoardID, bool) { } func CreateBoard(name string) BoardID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } bid := incGetBoardID() @@ -43,7 +43,7 @@ func checkAnonFee() bool { } func CreateThread(bid BoardID, title string, body string) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -61,7 +61,7 @@ func CreateThread(bid BoardID, title string, body string) PostID { } func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -91,7 +91,7 @@ func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { // If dstBoard is private, does not ping back. // If board specified by bid is private, panics. func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -121,7 +121,7 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar } func DeletePost(bid BoardID, threadid, postid PostID, reason string) { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -153,7 +153,7 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { } func EditPost(bid BoardID, threadid, postid PostID, title, body string) { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() diff --git a/examples/gno.land/r/demo/boards/z_0_a_filetest.gno b/examples/gno.land/r/demo/boards/z_0_a_filetest.gno index 5e8ff520a54..297231970a5 100644 --- a/examples/gno.land/r/demo/boards/z_0_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_a_filetest.gno @@ -2,12 +2,17 @@ package boards_test import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" ) var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") pid := boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_0_b_filetest.gno b/examples/gno.land/r/demo/boards/z_0_b_filetest.gno index 9bcbe9ffafa..4830161ac71 100644 --- a/examples/gno.land/r/demo/boards/z_0_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_b_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 19900000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") } diff --git a/examples/gno.land/r/demo/boards/z_0_c_filetest.gno b/examples/gno.land/r/demo/boards/z_0_c_filetest.gno index 99fd339aed8..d0b0930240d 100644 --- a/examples/gno.land/r/demo/boards/z_0_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_c_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") boards.CreateThread(1, "First Post (title)", "Body of the first post. (body)") } diff --git a/examples/gno.land/r/demo/boards/z_0_d_filetest.gno b/examples/gno.land/r/demo/boards/z_0_d_filetest.gno index c77e60e3f3a..7e21f83febd 100644 --- a/examples/gno.land/r/demo/boards/z_0_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_d_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") boards.CreateReply(bid, 0, 0, "Reply of the second post") diff --git a/examples/gno.land/r/demo/boards/z_0_e_filetest.gno b/examples/gno.land/r/demo/boards/z_0_e_filetest.gno index 6db036e87ba..bdf6d63727b 100644 --- a/examples/gno.land/r/demo/boards/z_0_e_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_e_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") boards.CreateReply(bid, 0, 0, "Reply of the second post") } diff --git a/examples/gno.land/r/demo/boards/z_0_filetest.gno b/examples/gno.land/r/demo/boards/z_0_filetest.gno index f56f6495b17..dc881ce46ad 100644 --- a/examples/gno.land/r/demo/boards/z_0_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 20000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") diff --git a/examples/gno.land/r/demo/boards/z_10_a_filetest.gno b/examples/gno.land/r/demo/boards/z_10_a_filetest.gno index ad57283bfcf..fc04555bf39 100644 --- a/examples/gno.land/r/demo/boards/z_10_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_a_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -23,6 +27,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // boardId 2 not exist boards.DeletePost(2, pid, pid, "") diff --git a/examples/gno.land/r/demo/boards/z_10_b_filetest.gno b/examples/gno.land/r/demo/boards/z_10_b_filetest.gno index cf8a332174f..2353268ef54 100644 --- a/examples/gno.land/r/demo/boards/z_10_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_b_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // pid of 2 not exist + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.DeletePost(bid, 2, 2, "") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno index 3fdd915a389..8f85cf63ecb 100644 --- a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.DeletePost(bid, pid, rid, "") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_10_filetest.gno b/examples/gno.land/r/demo/boards/z_10_filetest.gno index 80254592d5f..01e0f9439c7 100644 --- a/examples/gno.land/r/demo/boards/z_10_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -23,6 +27,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) boards.DeletePost(bid, pid, pid, "") println("----------------------------------------------------") diff --git a/examples/gno.land/r/demo/boards/z_11_a_filetest.gno b/examples/gno.land/r/demo/boards/z_11_a_filetest.gno index d7dc7b90782..b891e395fe6 100644 --- a/examples/gno.land/r/demo/boards/z_11_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_a_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // board 2 not exist + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(2, pid, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_11_b_filetest.gno b/examples/gno.land/r/demo/boards/z_11_b_filetest.gno index 3aa28095502..9322ac191c5 100644 --- a/examples/gno.land/r/demo/boards/z_11_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_b_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // thread 2 not exist + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(bid, 2, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_11_c_filetest.gno b/examples/gno.land/r/demo/boards/z_11_c_filetest.gno index df764303562..de4f828c1ca 100644 --- a/examples/gno.land/r/demo/boards/z_11_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_c_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // post 2 not exist + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(bid, pid, 2, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno index 0a5c1886d24..344583de7d4 100644 --- a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(bid, pid, rid, "", "Edited: First reply of the First post\n") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_11_filetest.gno b/examples/gno.land/r/demo/boards/z_11_filetest.gno index 1f04d7b686d..4cea63126c5 100644 --- a/examples/gno.land/r/demo/boards/z_11_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -24,6 +28,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(bid, pid, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_12_a_filetest.gno b/examples/gno.land/r/demo/boards/z_12_a_filetest.gno index 909be880efa..380e9bc09e0 100644 --- a/examples/gno.land/r/demo/boards/z_12_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_a_filetest.gno @@ -12,6 +12,8 @@ import ( ) func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") // create a post via registered user bid1 := boards.CreateBoard("test_board1") diff --git a/examples/gno.land/r/demo/boards/z_12_b_filetest.gno b/examples/gno.land/r/demo/boards/z_12_b_filetest.gno index 6b2166895c0..553108df9b3 100644 --- a/examples/gno.land/r/demo/boards/z_12_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_b_filetest.gno @@ -4,11 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid1 := boards.CreateBoard("test_board1") pid := boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_12_c_filetest.gno b/examples/gno.land/r/demo/boards/z_12_c_filetest.gno index 7397c487d7d..3b2e7ec04c0 100644 --- a/examples/gno.land/r/demo/boards/z_12_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_c_filetest.gno @@ -4,11 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid1 := boards.CreateBoard("test_board1") boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_12_d_filetest.gno b/examples/gno.land/r/demo/boards/z_12_d_filetest.gno index 37b6473f7ac..20818b05c0f 100644 --- a/examples/gno.land/r/demo/boards/z_12_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_d_filetest.gno @@ -4,11 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid1 := boards.CreateBoard("test_board1") pid := boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_12_filetest.gno b/examples/gno.land/r/demo/boards/z_12_filetest.gno index a362ea59823..cc4439c5934 100644 --- a/examples/gno.land/r/demo/boards/z_12_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -15,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid1 = boards.CreateBoard("test_board1") @@ -23,6 +28,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) rid := boards.CreateRepost(bid1, pid, "", "Check this out", bid2) println(rid) println(boards.Render("test_board2")) diff --git a/examples/gno.land/r/demo/boards/z_1_filetest.gno b/examples/gno.land/r/demo/boards/z_1_filetest.gno index 4d46c81b83d..6db254c661d 100644 --- a/examples/gno.land/r/demo/boards/z_1_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_1_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var board *boards.Board func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") _ = boards.CreateBoard("test_board_1") diff --git a/examples/gno.land/r/demo/boards/z_2_filetest.gno b/examples/gno.land/r/demo/boards/z_2_filetest.gno index 55f94e10f1a..bf8b643913c 100644 --- a/examples/gno.land/r/demo/boards/z_2_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_2_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") diff --git a/examples/gno.land/r/demo/boards/z_3_filetest.gno b/examples/gno.land/r/demo/boards/z_3_filetest.gno index c2aa264b1d2..4717bfd3958 100644 --- a/examples/gno.land/r/demo/boards/z_3_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_3_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -24,6 +28,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) rid := boards.CreateReply(bid, pid, pid, "Reply of the second post") println(rid) println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_4_filetest.gno b/examples/gno.land/r/demo/boards/z_4_filetest.gno index aede35077f9..e519e6babfb 100644 --- a/examples/gno.land/r/demo/boards/z_4_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) rid2 := boards.CreateReply(bid, pid, pid, "Second reply of the second post") println(rid2) println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_5_b_filetest.gno b/examples/gno.land/r/demo/boards/z_5_b_filetest.gno index e79da5c3677..0ad15ca2600 100644 --- a/examples/gno.land/r/demo/boards/z_5_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_b_filetest.gno @@ -14,6 +14,8 @@ import ( const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") // create board via registered user bid := boards.CreateBoard("test_board") diff --git a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno index 176c11da73c..abe8f1cf0bd 100644 --- a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno @@ -14,6 +14,8 @@ import ( const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") // create board via registered user bid := boards.CreateBoard("test_board") diff --git a/examples/gno.land/r/demo/boards/z_5_d_filetest.gno b/examples/gno.land/r/demo/boards/z_5_d_filetest.gno index 54cfe49eec6..33175efd4f2 100644 --- a/examples/gno.land/r/demo/boards/z_5_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_d_filetest.gno @@ -14,6 +14,8 @@ import ( const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") // create board via registered user bid := boards.CreateBoard("test_board") diff --git a/examples/gno.land/r/demo/boards/z_5_filetest.gno b/examples/gno.land/r/demo/boards/z_5_filetest.gno index 4f6f5cb9b75..04ef39e8938 100644 --- a/examples/gno.land/r/demo/boards/z_5_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) rid2 := boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) } diff --git a/examples/gno.land/r/demo/boards/z_6_filetest.gno b/examples/gno.land/r/demo/boards/z_6_filetest.gno index 39791606aae..8847c46130a 100644 --- a/examples/gno.land/r/demo/boards/z_6_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_6_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") boards.CreateReply(bid, pid, rid, "First reply of the first reply\n") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_7_filetest.gno b/examples/gno.land/r/demo/boards/z_7_filetest.gno index 9ff95f7492d..30f39351815 100644 --- a/examples/gno.land/r/demo/boards/z_7_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno @@ -4,11 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) // register users.Register("", "gnouser", "my profile") diff --git a/examples/gno.land/r/demo/boards/z_8_filetest.gno b/examples/gno.land/r/demo/boards/z_8_filetest.gno index 47d84737698..5824dbc8ad6 100644 --- a/examples/gno.land/r/demo/boards/z_8_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") rid2 := boards.CreateReply(bid, pid, rid, "First reply of the first reply\n") println(boards.Render("test_board/" + strconv.Itoa(int(pid)) + "/" + strconv.Itoa(int(rid2)))) diff --git a/examples/gno.land/r/demo/boards/z_9_a_filetest.gno b/examples/gno.land/r/demo/boards/z_9_a_filetest.gno index 8d07ba0e710..79f68f20200 100644 --- a/examples/gno.land/r/demo/boards/z_9_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_a_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var dstBoard boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") dstBoard = boards.CreateBoard("dst_board") diff --git a/examples/gno.land/r/demo/boards/z_9_b_filetest.gno b/examples/gno.land/r/demo/boards/z_9_b_filetest.gno index 68daf770b4f..703557cf476 100644 --- a/examples/gno.land/r/demo/boards/z_9_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_b_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -14,6 +17,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") srcBoard = boards.CreateBoard("first_board") diff --git a/examples/gno.land/r/demo/boards/z_9_filetest.gno b/examples/gno.land/r/demo/boards/z_9_filetest.gno index 96af90263d5..9e23258553e 100644 --- a/examples/gno.land/r/demo/boards/z_9_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") firstBoard = boards.CreateBoard("first_board") diff --git a/examples/gno.land/r/demo/tests/subtests/subtests.gno b/examples/gno.land/r/demo/tests/subtests/subtests.gno index 6bf43cba5eb..5043c704017 100644 --- a/examples/gno.land/r/demo/tests/subtests/subtests.gno +++ b/examples/gno.land/r/demo/tests/subtests/subtests.gno @@ -21,5 +21,5 @@ func CallAssertOriginCall() { } func CallIsOriginCall() bool { - return std.IsOriginCall() + return std.PrevRealm().IsUser() } diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index e7fde94ea08..cdeea62de66 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -32,7 +32,7 @@ func CallAssertOriginCall() { } func CallIsOriginCall() bool { - return std.IsOriginCall() + return std.PrevRealm().IsUser() } func CallSubtestsAssertOriginCall() { diff --git a/examples/gno.land/r/demo/tests/tests_test.gno b/examples/gno.land/r/demo/tests/tests_test.gno index ccbc6b91265..fa3872744c8 100644 --- a/examples/gno.land/r/demo/tests/tests_test.gno +++ b/examples/gno.land/r/demo/tests/tests_test.gno @@ -1,17 +1,23 @@ -package tests +package tests_test import ( "std" "testing" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/tests" ) func TestAssertOriginCall(t *testing.T) { // CallAssertOriginCall(): no panic - CallAssertOriginCall() - if !CallIsOriginCall() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) + tests.CallAssertOriginCall() + if !tests.CallIsOriginCall() { t.Errorf("expected IsOriginCall=true but got false") } + std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/tests")) // CallAssertOriginCall() from a block: panic expectedReason := "invalid non-origin call" func() { @@ -23,10 +29,10 @@ func TestAssertOriginCall(t *testing.T) { }() // if called inside a function literal, this is no longer an origin call // because there's one additional frame (the function literal block). - if CallIsOriginCall() { + if tests.CallIsOriginCall() { t.Errorf("expected IsOriginCall=false but got true") } - CallAssertOriginCall() + tests.CallAssertOriginCall() }() // CallSubtestsAssertOriginCall(): panic @@ -36,23 +42,24 @@ func TestAssertOriginCall(t *testing.T) { t.Errorf("expected panic with '%v', got '%v'", expectedReason, r) } }() - if CallSubtestsIsOriginCall() { + if tests.CallSubtestsIsOriginCall() { t.Errorf("expected IsOriginCall=false but got true") } - CallSubtestsAssertOriginCall() + tests.CallSubtestsAssertOriginCall() } func TestPrevRealm(t *testing.T) { var ( - user1Addr = std.DerivePkgAddr("user1.gno") + firstRealm = std.DerivePkgAddr("gno.land/r/demo/tests_test") rTestsAddr = std.DerivePkgAddr("gno.land/r/demo/tests") ) - // When a single realm in the frames, PrevRealm returns the user - if addr := GetPrevRealm().Addr(); addr != user1Addr { - t.Errorf("want GetPrevRealm().Addr==%s, got %s", user1Addr, addr) + // When only one realm in the frames, PrevRealm returns the same realm + if addr := tests.GetPrevRealm().Addr(); addr != firstRealm { + println(tests.GetPrevRealm()) + t.Errorf("want GetPrevRealm().Addr==%s, got %s", firstRealm, addr) } // When 2 or more realms in the frames, PrevRealm returns the second to last - if addr := GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr { + if addr := tests.GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr { t.Errorf("want GetRSubtestsPrevRealm().Addr==%s, got %s", rTestsAddr, addr) } } diff --git a/examples/gno.land/r/demo/users/z_11_filetest.gno b/examples/gno.land/r/demo/users/z_11_filetest.gno index 27c7e9813da..212dc169007 100644 --- a/examples/gno.land/r/demo/users/z_11_filetest.gno +++ b/examples/gno.land/r/demo/users/z_11_filetest.gno @@ -11,8 +11,8 @@ import ( const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { - caller := std.GetOrigCaller() // main std.TestSetOrigCaller(admin) + caller := std.GetOrigCaller() // main users.AdminAddRestrictedName("superrestricted") // test restricted name diff --git a/examples/gno.land/r/demo/users/z_11b_filetest.gno b/examples/gno.land/r/demo/users/z_11b_filetest.gno index be508963911..6041f4b7113 100644 --- a/examples/gno.land/r/demo/users/z_11b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_11b_filetest.gno @@ -11,8 +11,8 @@ import ( const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { - caller := std.GetOrigCaller() // main std.TestSetOrigCaller(admin) + caller := std.GetOrigCaller() // main // add restricted name users.AdminAddRestrictedName("superrestricted") // grant invite to caller diff --git a/examples/gno.land/r/demo/users/z_5_filetest.gno b/examples/gno.land/r/demo/users/z_5_filetest.gno index 6465cc9c378..9080b509b8e 100644 --- a/examples/gno.land/r/demo/users/z_5_filetest.gno +++ b/examples/gno.land/r/demo/users/z_5_filetest.gno @@ -16,14 +16,17 @@ func main() { users.Register("", "gnouser", "my profile") // as admin, grant invites to gnouser std.TestSetOrigCaller(admin) + std.TestSetRealm(std.NewUserRealm(admin)) users.GrantInvites(caller.String() + ":1") // switch back to caller std.TestSetOrigCaller(caller) + std.TestSetRealm(std.NewUserRealm(caller)) // invite another addr test1 := testutils.TestAddress("test1") users.Invite(test1.String()) // switch to test1 std.TestSetOrigCaller(test1) + std.TestSetRealm(std.NewUserRealm(test1)) std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) users.Register(caller, "satoshi", "my other profile") println(users.Render("")) diff --git a/examples/gno.land/r/demo/users/z_6_filetest.gno b/examples/gno.land/r/demo/users/z_6_filetest.gno index 919088088a2..e6a63d83358 100644 --- a/examples/gno.land/r/demo/users/z_6_filetest.gno +++ b/examples/gno.land/r/demo/users/z_6_filetest.gno @@ -9,7 +9,7 @@ import ( const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { - caller := std.GetOrigCaller() + caller := std.GetOrigCaller() // main // as admin, grant invites to unregistered user. std.TestSetOrigCaller(admin) users.GrantInvites(caller.String() + ":1") diff --git a/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar index 906c43cc41b..5e1f1a5ff0f 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar @@ -36,7 +36,7 @@ package main import "std" func hello() { - std.AssertOriginCall() + std.GetChainID() } -- main.gno.gen.go.golden -- @@ -61,5 +61,5 @@ package main import "github.com/gnolang/gno/gnovm/stdlibs/std" func hello() { - std.AssertOriginCall(nil) + std.GetChainID(nil) } diff --git a/gnovm/pkg/transpiler/transpiler_test.go b/gnovm/pkg/transpiler/transpiler_test.go index 2a0707f7f79..63b77e49446 100644 --- a/gnovm/pkg/transpiler/transpiler_test.go +++ b/gnovm/pkg/transpiler/transpiler_test.go @@ -344,13 +344,13 @@ func Float32bits(i float32) uint32 func testfunc() { println(Float32bits(3.14159)) - std.AssertOriginCall() + std.GetChainID() } func otherFunc() { std := 1 // This is (incorrectly) changed for now. - std.AssertOriginCall() + std.GetChainID() } `, expectedOutput: ` @@ -363,13 +363,13 @@ import "github.com/gnolang/gno/gnovm/stdlibs/std" func testfunc() { println(Float32bits(3.14159)) - std.AssertOriginCall(nil) + std.GetChainID(nil) } func otherFunc() { std := 1 // This is (incorrectly) changed for now. - std.AssertOriginCall(nil) + std.GetChainID(nil) } `, expectedImports: []*ast.ImportSpec{ @@ -388,11 +388,11 @@ func otherFunc() { source: ` package std -func AssertOriginCall() +func GetChainID() func origCaller() string func testfunc() { - AssertOriginCall() + GetChainID() println(origCaller()) } `, @@ -403,7 +403,7 @@ func testfunc() { package std func testfunc() { - AssertOriginCall(nil) + GetChainID(nil) println(X_origCaller(nil)) } `, diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index 6bd45de3589..a349ddf092e 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -428,26 +428,6 @@ var nativeFuncs = [...]NativeFunc{ ) }, }, - { - "std", - "IsOriginCall", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{ - {Name: gno.N("r0"), Type: gno.X("bool")}, - }, - true, - func(m *gno.Machine) { - r0 := libs_std.IsOriginCall( - m, - ) - - m.PushValue(gno.Go2GnoValue( - m.Alloc, - m.Store, - reflect.ValueOf(&r0).Elem(), - )) - }, - }, { "std", "GetChainID", diff --git a/gnovm/stdlibs/std/native.gno b/gnovm/stdlibs/std/native.gno index 9cf8808a07e..2baa1f92f48 100644 --- a/gnovm/stdlibs/std/native.gno +++ b/gnovm/stdlibs/std/native.gno @@ -1,15 +1,11 @@ package std -// AssertOriginCall panics if [IsOriginCall] returns false. -func AssertOriginCall() // injected - -// IsOriginCall returns true only if the calling method is invoked via a direct -// MsgCall. It returns false for all other cases, like if the calling method +// AssertOriginCall panics if the calling method is not invoked via a direct +// MsgCall. It panics for for other cases, like if the calling method // is invoked by another method (even from the same realm or package). -// It also returns false every time when the transaction is broadcasted via +// It also panic every time when the transaction is broadcasted via // MsgRun. -func IsOriginCall() bool // injected - +func AssertOriginCall() // injected func GetChainID() string // injected func GetChainDomain() string // injected func GetHeight() int64 // injected diff --git a/gnovm/stdlibs/std/native.go b/gnovm/stdlibs/std/native.go index 9e398e907a2..68f4542f689 100644 --- a/gnovm/stdlibs/std/native.go +++ b/gnovm/stdlibs/std/native.go @@ -7,12 +7,12 @@ import ( ) func AssertOriginCall(m *gno.Machine) { - if !IsOriginCall(m) { + if !isOriginCall(m) { m.Panic(typedString("invalid non-origin call")) } } -func IsOriginCall(m *gno.Machine) bool { +func isOriginCall(m *gno.Machine) bool { n := m.NumFrames() if n == 0 { return false diff --git a/gnovm/stdlibs/std/native_test.go b/gnovm/stdlibs/std/native_test.go index 851785575d7..acbd22055d6 100644 --- a/gnovm/stdlibs/std/native_test.go +++ b/gnovm/stdlibs/std/native_test.go @@ -184,7 +184,7 @@ func TestPrevRealmIsOrigin(t *testing.T) { assert := assert.New(t) addr, pkgPath := X_getRealm(tt.machine, 1) - isOrigin := IsOriginCall(tt.machine) + isOrigin := isOriginCall(tt.machine) assert.Equal(string(tt.expectedAddr), addr) assert.Equal(tt.expectedPkgPath, pkgPath) diff --git a/gnovm/tests/files/std5.gno b/gnovm/tests/files/std5.gno index 2f9e98bb4ec..1f1d013c3df 100644 --- a/gnovm/tests/files/std5.gno +++ b/gnovm/tests/files/std5.gno @@ -13,10 +13,10 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(2) -// std/native.gno:45 +// std/native.gno:41 // main() // main/files/std5.gno:10 diff --git a/gnovm/tests/files/std8.gno b/gnovm/tests/files/std8.gno index dfc2b8ca5fd..3d0e4a7085e 100644 --- a/gnovm/tests/files/std8.gno +++ b/gnovm/tests/files/std8.gno @@ -23,10 +23,10 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(4) -// std/native.gno:45 +// std/native.gno:41 // fn() // main/files/std8.gno:16 // testutils.WrapCall(inner) diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go index 4445d2467e8..4690f47d82f 100644 --- a/gnovm/tests/stdlibs/generated.go +++ b/gnovm/tests/stdlibs/generated.go @@ -43,26 +43,6 @@ var nativeFuncs = [...]NativeFunc{ ) }, }, - { - "std", - "IsOriginCall", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{ - {Name: gno.N("r0"), Type: gno.X("bool")}, - }, - true, - func(m *gno.Machine) { - r0 := testlibs_std.IsOriginCall( - m, - ) - - m.PushValue(gno.Go2GnoValue( - m.Alloc, - m.Store, - reflect.ValueOf(&r0).Elem(), - )) - }, - }, { "std", "TestSkipHeights", diff --git a/gnovm/tests/stdlibs/std/std.gno b/gnovm/tests/stdlibs/std/std.gno index dcb5a64dbb3..c30071313fe 100644 --- a/gnovm/tests/stdlibs/std/std.gno +++ b/gnovm/tests/stdlibs/std/std.gno @@ -1,7 +1,6 @@ package std func AssertOriginCall() // injected -func IsOriginCall() bool // injected func TestSkipHeights(count int64) // injected func TestSetOrigCaller(addr Address) { testSetOrigCaller(string(addr)) } diff --git a/gnovm/tests/stdlibs/std/std.go b/gnovm/tests/stdlibs/std/std.go index 675194b252f..eac51c5fb0e 100644 --- a/gnovm/tests/stdlibs/std/std.go +++ b/gnovm/tests/stdlibs/std/std.go @@ -26,7 +26,7 @@ type RealmOverride struct { } func AssertOriginCall(m *gno.Machine) { - if !IsOriginCall(m) { + if !isOriginCall(m) { m.Panic(typedString("invalid non-origin call")) } } @@ -37,7 +37,7 @@ func typedString(s gno.StringValue) gno.TypedValue { return tv } -func IsOriginCall(m *gno.Machine) bool { +func isOriginCall(m *gno.Machine) bool { tname := m.Frames[0].Func.Name switch tname { case "main": // test is a _filetest From 479b314fd60e5952c75ad4953e1ea298546fd0b3 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Tue, 4 Feb 2025 22:13:45 +0100 Subject: [PATCH 20/60] feat(gnovm): support constant evaluation of len and cap on arrays (#3600) Closes: #3201 This update introduces the ability to evaluate len and cap for arrays at preprocess-time, allowing these values to be treated as constants. While the array itself is not constant, the values of len and cap can be determined during the preprocess. This change eliminates the need for machine.EvalStatic that causes a vm crash. --------- Co-authored-by: Lee ByeongJun Co-authored-by: Petar Dambovaliev --- gnovm/pkg/gnolang/preprocess.go | 42 ++++++++++++++++++++++++++------- gnovm/pkg/gnolang/type_check.go | 4 ++-- gnovm/pkg/gnolang/types.go | 7 ++++++ gnovm/tests/files/const51.gno | 20 ++++++++++++++++ gnovm/tests/files/const52.gno | 11 +++++++++ 5 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 gnovm/tests/files/const51.gno create mode 100644 gnovm/tests/files/const52.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index ca5834aa44e..0b86449b235 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3366,18 +3366,42 @@ func getResultTypedValues(cx *CallExpr) []TypedValue { // NOTE: Generally, conversion happens in a separate step while leaving // composite exprs/nodes that contain constant expression nodes (e.g. const // exprs in the rhs of AssignStmts). +// +// Array-related expressions like `len` and `cap` are manually evaluated +// as constants, even if the array itself is not a constant. This evaluation +// is handled independently of the rest of the constant evaluation process, +// bypassing machine.EvalStatic. func evalConst(store Store, last BlockNode, x Expr) *ConstExpr { // TODO: some check or verification for ensuring x - // is constant? From the machine? - m := NewMachine(".dontcare", store) - m.PreprocessorMode = true + var cx *ConstExpr + if clx, ok := x.(*CallExpr); ok { + t := evalStaticTypeOf(store, last, clx.Args[0]) + if ar, ok := unwrapPointerType(baseOf(t)).(*ArrayType); ok { + fv := clx.Func.(*ConstExpr).V.(*FuncValue) + switch fv.Name { + case "cap", "len": + tv := TypedValue{T: IntType} + tv.SetInt(ar.Len) + cx = &ConstExpr{ + Source: x, + TypedValue: tv, + } + default: + panic(fmt.Sprintf("unexpected const func %s", fv.Name)) + } + } + } - cv := m.EvalStatic(last, x) - m.PreprocessorMode = false - m.Release() - cx := &ConstExpr{ - Source: x, - TypedValue: cv, + if cx == nil { + // is constant? From the machine? + m := NewMachine(".dontcare", store) + cv := m.EvalStatic(last, x) + m.PreprocessorMode = false + m.Release() + cx = &ConstExpr{ + Source: x, + TypedValue: cv, + } } cx.SetLine(x.GetLine()) cx.SetAttribute(ATTR_PREPROCESSED, true) diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index f96cb71e4b6..a79e9c43ecc 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -270,7 +270,7 @@ Main: switch { case fv.Name == "len": at := evalStaticTypeOf(store, last, currExpr.Args[0]) - if _, ok := baseOf(at).(*ArrayType); ok { + if _, ok := unwrapPointerType(baseOf(at)).(*ArrayType); ok { // ok break Main } @@ -278,7 +278,7 @@ Main: break Main case fv.Name == "cap": at := evalStaticTypeOf(store, last, currExpr.Args[0]) - if _, ok := baseOf(at).(*ArrayType); ok { + if _, ok := unwrapPointerType(baseOf(at)).(*ArrayType); ok { // ok break Main } diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 374ac6d9150..8ac07162f10 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -1469,6 +1469,13 @@ func baseOf(t Type) Type { } } +func unwrapPointerType(t Type) Type { + if pt, ok := t.(*PointerType); ok { + return pt.Elem() + } + return t +} + // NOTE: it may be faster to switch on baseOf(). func (dt *DeclaredType) Kind() Kind { return dt.Base.Kind() diff --git a/gnovm/tests/files/const51.gno b/gnovm/tests/files/const51.gno new file mode 100644 index 00000000000..b00748b0ec7 --- /dev/null +++ b/gnovm/tests/files/const51.gno @@ -0,0 +1,20 @@ +package main + +type T1 struct { + x [2]string +} + +type T2 struct { + x *[2]string +} + +func main() { + t1 := T1{x: [2]string{"a", "b"}} + t2 := T2{x: &[2]string{"a", "b"}} + const c1 = len(t1.x) + const c2 = len(t2.x) + println(c1, c2) +} + +// Output: +// 2 2 diff --git a/gnovm/tests/files/const52.gno b/gnovm/tests/files/const52.gno new file mode 100644 index 00000000000..c213faeb12b --- /dev/null +++ b/gnovm/tests/files/const52.gno @@ -0,0 +1,11 @@ +package main + +func main() { + s := make([][2]string, 1) // Slice with length 1 + s[0] = [2]string{"a", "b"} // Assign value to s[0] + const r = len(s[0]) + println(r) // Prints: 2 +} + +// Output: +// 2 From 3614b605cb79a29bfdeabde2f9732f3b901d1418 Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 4 Feb 2025 22:15:29 +0100 Subject: [PATCH 21/60] fix(github-bot): require most rules to activate when base == `master` (#3592) Co-authored-by: Antoine Eddi <5222525+aeddi@users.noreply.github.com> --- contribs/github-bot/internal/config/config.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index 43a505ad15a..5db91a73413 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -33,12 +33,18 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { auto := []AutomaticCheck{ { Description: "Maintainers must be able to edit this pull request ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork))", - If: c.CreatedFromFork(), - Then: r.MaintainerCanModify(), + If: c.And( + c.BaseBranch("^master$"), + c.CreatedFromFork(), + ), + Then: r.MaintainerCanModify(), }, { Description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff", - If: c.FileChanged(gh, "^docs/"), + If: c.And( + c.BaseBranch("^master$"), + c.FileChanged(gh, "^docs/"), + ), Then: r.And( r.Or( r.AuthorInTeam(gh, "tech-staff"), @@ -57,7 +63,10 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { }, { Description: "Pending initial approval by a review team member, or review from tech-staff", - If: c.Not(c.AuthorInTeam(gh, "tech-staff")), + If: c.And( + c.BaseBranch("^master$"), + c.Not(c.AuthorInTeam(gh, "tech-staff")), + ), Then: r. If(r.Or( r.ReviewByOrgMembers(gh).WithDesiredState(utils.ReviewStateApproved), @@ -91,7 +100,7 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { { Description: "Determine if infra needs to be updated before merging", If: c.And( - c.BaseBranch("master"), + c.BaseBranch("^master$"), c.Or( c.FileChanged(gh, `Dockerfile`), c.FileChanged(gh, `^misc/deployments`), From 8410060e33725bf99b7417088b140895eea43485 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 4 Feb 2025 22:23:45 +0100 Subject: [PATCH 22/60] feat: add r/moul/present (#3668) This realm is a good example of reusing and integrating some of my recently added libraries: - `p/moul/md` #2819 - `p/moul/mdtable` #3100 - `p/moul/realmpath` #3257 - `p/moul/txlink` #3289 - `p/moul/collection` #3321 - `p/demo/avl/pager` #2584 It helped me identify that `txlink` was not escaping the arguments, which resulted in invalid links. (fixed in #3682) Additionally, it provided me with a better understanding of: - The shortcomings of the `p/moul/md` API, particularly regarding `"\n"` handling - The need for improved management of the pager for `p/moul/collection` - What kind of UI improvements we could need on gnoweb. #3355 Demo: https://github.com/user-attachments/assets/4b20cee8-b8d7-4eba-90a8-5b87a3c19521 I also suggest you to look at the `filetest.gno` file. I believe we should proceed with the merge, to inspire others to create similar composed realms. However, I have a few improvement ideas: 1. Extract most of the generic logic into a `p/moul/present`. 2. Consider either making r/moul/present importable by `r/coreteam/present` to create a hub for presentations from all teammates, or the opposite: make `r/coreteam/present` the content source and allow `r/moul/present` to display a subset where `author="moul"` What are your thoughts? 3. Clean up the code using an improved `p/moul/md` and possibly new `p/` generic utilities. Depenes on #3682 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/r/moul/present/admin.gno | 96 ----- examples/gno.land/r/moul/present/present.gno | 353 ++++++++++++++++++ .../r/moul/present/present_filetest.gno | 233 ++++++++++++ .../gno.land/r/moul/present/present_init.gno | 25 ++ .../r/moul/present/present_miami23.gno | 42 --- .../moul/present/present_miami23_filetest.gno | 11 - .../gno.land/r/moul/present/presentations.gno | 17 - 7 files changed, 611 insertions(+), 166 deletions(-) delete mode 100644 examples/gno.land/r/moul/present/admin.gno create mode 100644 examples/gno.land/r/moul/present/present.gno create mode 100644 examples/gno.land/r/moul/present/present_filetest.gno create mode 100644 examples/gno.land/r/moul/present/present_init.gno delete mode 100644 examples/gno.land/r/moul/present/present_miami23.gno delete mode 100644 examples/gno.land/r/moul/present/present_miami23_filetest.gno delete mode 100644 examples/gno.land/r/moul/present/presentations.gno diff --git a/examples/gno.land/r/moul/present/admin.gno b/examples/gno.land/r/moul/present/admin.gno deleted file mode 100644 index ab99b1725c5..00000000000 --- a/examples/gno.land/r/moul/present/admin.gno +++ /dev/null @@ -1,96 +0,0 @@ -package present - -import ( - "std" - "strings" - - "gno.land/p/demo/avl" -) - -var ( - adminAddr std.Address - moderatorList avl.Tree - inPause bool -) - -func init() { - // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" -} - -func AdminSetAdminAddr(addr std.Address) { - assertIsAdmin() - adminAddr = addr -} - -func AdminSetInPause(state bool) { - assertIsAdmin() - inPause = state -} - -func AdminAddModerator(addr std.Address) { - assertIsAdmin() - moderatorList.Set(addr.String(), true) -} - -func AdminRemoveModerator(addr std.Address) { - assertIsAdmin() - moderatorList.Set(addr.String(), false) // XXX: delete instead? -} - -func ModAddPost(slug, title, body, publicationDate, authors, tags string) { - assertIsModerator() - - caller := std.GetOrigCaller() - tagList := strings.Split(tags, ",") - authorList := strings.Split(authors, ",") - - err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList) - checkErr(err) -} - -func ModEditPost(slug, title, body, publicationDate, authors, tags string) { - assertIsModerator() - - tagList := strings.Split(tags, ",") - authorList := strings.Split(authors, ",") - - err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList) - checkErr(err) -} - -func isAdmin(addr std.Address) bool { - return addr == adminAddr -} - -func isModerator(addr std.Address) bool { - _, found := moderatorList.Get(addr.String()) - return found -} - -func assertIsAdmin() { - caller := std.GetOrigCaller() - if !isAdmin(caller) { - panic("access restricted.") - } -} - -func assertIsModerator() { - caller := std.GetOrigCaller() - if isAdmin(caller) || isModerator(caller) { - return - } - panic("access restricted") -} - -func assertNotInPause() { - if inPause { - panic("access restricted (pause)") - } -} - -func checkErr(err error) { - if err != nil { - panic(err) - } -} diff --git a/examples/gno.land/r/moul/present/present.gno b/examples/gno.land/r/moul/present/present.gno new file mode 100644 index 00000000000..b4f880318bf --- /dev/null +++ b/examples/gno.land/r/moul/present/present.gno @@ -0,0 +1,353 @@ +package present + +import ( + "net/url" + "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ownable" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/collection" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/realmpath" + "gno.land/p/moul/txlink" +) + +type Presentation struct { + Slug string + Title string + Event string + Author string + Uploader std.Address + Date time.Time + Content string + EditDate time.Time + NumSlides int +} + +var ( + presentations *collection.Collection + Ownable *ownable.Ownable +) + +func init() { + presentations = collection.New() + // for /view and /slides + presentations.AddIndex("slug", func(v interface{}) string { + return v.(*Presentation).Slug + }, collection.UniqueIndex) + + // for table sorting + presentations.AddIndex("date", func(v interface{}) string { + return v.(*Presentation).Date.String() + }, collection.DefaultIndex) + presentations.AddIndex("author", func(v interface{}) string { + return v.(*Presentation).Author + }, collection.DefaultIndex) + presentations.AddIndex("title", func(v interface{}) string { + return v.(*Presentation).Title + }, collection.DefaultIndex) + + Ownable = ownable.New() +} + +// Render handles the realm's rendering logic +func Render(path string) string { + req := realmpath.Parse(path) + + // Get slug from path + slug := req.PathPart(0) + + // List view (home) + if slug == "" { + return renderList(req) + } + + // Slides view + if req.PathPart(1) == "slides" { + page := 1 + if pageStr := req.Query.Get("page"); pageStr != "" { + var err error + page, err = strconv.Atoi(pageStr) + if err != nil { + return "400: invalid page number" + } + } + return renderSlides(slug, page) + } + + // Regular view + return renderView(slug) +} + +// Set adds or updates a presentation +func Set(slug, title, event, author, date, content string) string { + Ownable.AssertCallerIsOwner() + + parsedDate, err := time.Parse("2006-01-02", date) + if err != nil { + return "400: invalid date format (expected: YYYY-MM-DD)" + } + + numSlides := 1 // Count intro slide + for _, line := range strings.Split(content, "\n") { + if strings.HasPrefix(line, "## ") { + numSlides++ + } + } + numSlides++ // Count thank you slide + + p := &Presentation{ + Slug: slug, + Title: title, + Event: event, + Author: author, + Uploader: std.PrevRealm().Addr(), + Date: parsedDate, + Content: content, + EditDate: time.Now(), + NumSlides: numSlides, + } + + presentations.Set(p) + return "presentation saved successfully" +} + +// Delete removes a presentation +func Delete(slug string) string { + Ownable.AssertCallerIsOwner() + + entry := presentations.GetFirst("slug", slug) + if entry == nil { + return "404: presentation not found" + } + + // XXX: consider this: + // if entry.Obj.(*Presentation).Uploader != std.PrevRealm().Addr() { + // return "401: unauthorized - only the uploader can delete their presentations" + // } + + // Convert the entry's ID from string to uint64 and delete + numericID, err := seqid.FromString(entry.ID) + if err != nil { + return "500: invalid entry ID format" + } + + presentations.Delete(uint64(numericID)) + return "presentation deleted successfully" +} + +func renderList(req *realmpath.Request) string { + var out strings.Builder + out.WriteString(md.H1("Presentations")) + + // Setup pager + index := presentations.GetIndex(getSortField(req)) + pgr := pager.NewPager(index, 10, isSortReversed(req)) + + // Get current page + page := pgr.MustGetPageByPath(req.String()) + + // Create table + dateColumn := renderSortLink(req, "date", "Date") + titleColumn := renderSortLink(req, "title", "Title") + authorColumn := renderSortLink(req, "author", "Author") + table := mdtable.Table{ + Headers: []string{dateColumn, titleColumn, "Event", authorColumn, "Slides"}, + } + + // Add rows from current page + for _, item := range page.Items { + // Get the actual presentation using the ID from the index + // XXX: improve p/moul/collection to make this more convenient. + // - no need to make per-id lookup. + // - transparently support multi-values. + // - integrate a sortable pager? + var ids []string + if ids_, ok := item.Value.([]string); ok { + ids = ids_ + } else if id, ok := item.Value.(string); ok { + ids = []string{id} + } + + for _, id := range ids { + entry := presentations.GetFirst(collection.IDIndex, id) + if entry == nil { + continue + } + p := entry.Obj.(*Presentation) + + table.Append([]string{ + p.Date.Format("2006-01-02"), + md.Link(p.Title, localPath(p.Slug, nil)), + p.Event, + p.Author, + ufmt.Sprintf("%d", p.NumSlides), + }) + } + } + + out.WriteString(table.String()) + out.WriteString(page.Picker()) // XXX: picker is not preserving the previous flags, should take "req" as argument. + return out.String() +} + +func (p *Presentation) FirstSlide() string { + var out strings.Builder + out.WriteString(md.H1(p.Title)) + out.WriteString(md.Paragraph(md.Bold(p.Event) + ", " + p.Date.Format("2 Jan 2006"))) + out.WriteString(md.Paragraph("by " + md.Bold(p.Author))) // XXX: link to u/? + return out.String() +} + +func (p *Presentation) LastSlide() string { + var out strings.Builder + out.WriteString(md.H1(p.Title)) + out.WriteString(md.H2("Thank You!")) + out.WriteString(md.Paragraph(p.Author)) + fullPath := "https://" + std.GetChainDomain() + localPath(p.Slug, nil) + out.WriteString(md.Paragraph("🔗 " + md.Link(fullPath, fullPath))) + // XXX: QRCode + return out.String() +} + +func renderView(slug string) string { + if slug == "" { + return "400: missing presentation slug" + } + + entry := presentations.GetFirst("slug", slug) + if entry == nil { + return "404: presentation not found" + } + + p := entry.Obj.(*Presentation) + var out strings.Builder + + // Header using FirstSlide helper + out.WriteString(p.FirstSlide()) + + // Slide mode link + out.WriteString(md.Link("View as slides", localPath(p.Slug+"/slides", nil)) + "\n\n") + out.WriteString(md.HorizontalRule()) + out.WriteString(md.Paragraph(p.Content)) + + // Metadata footer + out.WriteString(md.HorizontalRule()) + out.WriteString(ufmt.Sprintf("Last edited: %s\n\n", p.EditDate.Format("2006-01-02 15:04:05"))) + out.WriteString(ufmt.Sprintf("Uploader: `%s`\n\n", p.Uploader)) + out.WriteString(ufmt.Sprintf("Number of slides: %d\n\n", p.NumSlides)) + + // Admin actions + // XXX: consider a dynamic toggle for admin actions + editLink := txlink.Call("Set", + "slug", p.Slug, + "title", p.Title, + "author", p.Author, + "event", p.Event, + "date", p.Date.Format("2006-01-02"), + ) + deleteLink := txlink.Call("Delete", "slug", p.Slug) + out.WriteString(md.Paragraph(md.Link("Edit", editLink) + " | " + md.Link("Delete", deleteLink))) + + return out.String() +} + +// renderSlidesNavigation returns the navigation bar for slides +func renderSlidesNavigation(slug string, currentPage, totalSlides int) string { + var out strings.Builder + if currentPage > 1 { + prevLink := localPath(slug+"/slides", url.Values{"page": {ufmt.Sprintf("%d", currentPage-1)}}) + out.WriteString(md.Link("← Prev", prevLink) + " ") + } + out.WriteString(ufmt.Sprintf("| %d/%d |", currentPage, totalSlides)) + if currentPage < totalSlides { + nextLink := localPath(slug+"/slides", url.Values{"page": {ufmt.Sprintf("%d", currentPage+1)}}) + out.WriteString(" " + md.Link("Next →", nextLink)) + } + return md.Paragraph(out.String()) +} + +func renderSlides(slug string, currentPage int) string { + if slug == "" { + return "400: missing presentation ID" + } + + entry := presentations.GetFirst("slug", slug) + if entry == nil { + return "404: presentation not found" + } + + p := entry.Obj.(*Presentation) + slides := strings.Split("\n"+p.Content, "\n## ") + if currentPage < 1 || currentPage > p.NumSlides { + return "404: invalid slide number" + } + + var out strings.Builder + + // Display current slide + if currentPage == 1 { + out.WriteString(p.FirstSlide()) + } else if currentPage == p.NumSlides { + out.WriteString(p.LastSlide()) + } else { + out.WriteString(md.H1(p.Title)) + out.WriteString("## " + slides[currentPage-1] + "\n\n") + } + + out.WriteString(renderSlidesNavigation(slug, currentPage, p.NumSlides)) + return out.String() +} + +// Helper functions for sorting and pagination +func getSortField(req *realmpath.Request) string { + field := req.Query.Get("sort") + switch field { + case "date", "slug", "author", "title": + return field + } + return "date" +} + +func isSortReversed(req *realmpath.Request) bool { + return req.Query.Get("order") != "asc" +} + +func renderSortLink(req *realmpath.Request, field, label string) string { + currentField := getSortField(req) + currentOrder := req.Query.Get("order") + + newOrder := "desc" + if field == currentField && currentOrder != "asc" { + newOrder = "asc" + } + + query := req.Query + query.Set("sort", field) + query.Set("order", newOrder) + + if field == currentField { + if newOrder == "asc" { + label += " ↑" + } else { + label += " ↓" + } + } + + return md.Link(label, "?"+query.Encode()) +} + +// helper to create local realm links +func localPath(path string, query url.Values) string { + req := &realmpath.Request{ + Path: path, + Query: query, + } + return req.String() +} diff --git a/examples/gno.land/r/moul/present/present_filetest.gno b/examples/gno.land/r/moul/present/present_filetest.gno new file mode 100644 index 00000000000..7e9385454b9 --- /dev/null +++ b/examples/gno.land/r/moul/present/present_filetest.gno @@ -0,0 +1,233 @@ +package main + +import ( + "gno.land/r/moul/present" +) + +func main() { + // Cleanup initial state + ret := present.Delete("demo") + if ret != "presentation deleted successfully" { + panic("internal error") + } + + // Create presentations with IDs from 10-20 + presentations := []struct { + id string + title string + event string + author string + date string + content string + }{ + {"s10", "title10", "event3", "author1", "2024-01-01", "## s10.0\n## s10.1"}, + {"s11", "title11", "event1", "author2", "2024-01-15", "## s11.0\n## s11.1"}, + {"s12", "title12", "event2", "author1", "2024-02-01", "## s12.0\n## s12.1"}, + {"s13", "title13", "event1", "author3", "2024-01-20", "## s13.0\n## s13.1"}, + {"s14", "title14", "event3", "author2", "2024-03-01", "## s14.0\n## s14.1"}, + {"s15", "title15", "event2", "author1", "2024-02-15", "## s15.0\n## s15.1\n## s15.2"}, + {"s16", "title16", "event1", "author4", "2024-03-15", "## s16.0\n## s16.1"}, + {"s17", "title17", "event3", "author2", "2024-01-10", "## s17.0\n## s17.1"}, + {"s18", "title18", "event2", "author3", "2024-02-20", "## s18.0\n## s18.1"}, + {"s19", "title19", "event1", "author1", "2024-03-10", "## s19.0\n## s19.1"}, + {"s20", "title20", "event3", "author4", "2024-01-05", "## s20.0\n## s20.1"}, + } + + for _, p := range presentations { + result := present.Set(p.id, p.title, p.event, p.author, p.date, p.content) + if result != "presentation saved successfully" { + panic("failed to add presentation: " + result) + } + } + + // Test different sorting scenarios + printRender("") // default + printRender("?order=asc&sort=date") // by date ascending + printRender("?order=asc&sort=title") // by title ascending + printRender("?order=asc&sort=author") // by author ascending (multiple entries per author) + + // Test pagination + printRender("?order=asc&sort=title&page=2") // second page + + // Test view + printRender("s15") // view by slug + + // Test slides + printRender("s15/slides") // slides by slug + printRender("s15/slides?page=2") // slides by slug, second page + printRender("s15/slides?page=3") // slides by slug, third page + printRender("s15/slides?page=4") // slides by slug, fourth page + printRender("s15/slides?page=5") // slides by slug, fifth page +} + +// Helper function to print path and render result +func printRender(path string) { + println("+-------------------------------") + println("| PATH:", path) + println("| RESULT:\n" + present.Render(path) + "\n") +} + +// Output: +// +------------------------------- +// | PATH: +// | RESULT: +// # Presentations +// | [Date ↑](?order=asc&sort=date) | [Title](?order=desc&sort=title) | Event | [Author](?order=desc&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 | +// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 | +// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 | +// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 | +// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 | +// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 | +// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 | +// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 | +// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 | +// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 | +// **1** | [2](?page=2) +// +// +------------------------------- +// | PATH: ?order=asc&sort=date +// | RESULT: +// # Presentations +// | [Date ↓](?order=desc&sort=date) | [Title](?order=desc&sort=title) | Event | [Author](?order=desc&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 | +// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 | +// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 | +// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 | +// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 | +// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 | +// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 | +// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 | +// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 | +// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 | +// **1** | [2](?page=2) +// +// +------------------------------- +// | PATH: ?order=asc&sort=title +// | RESULT: +// # Presentations +// | [Date](?order=desc&sort=date) | [Title](?order=desc&sort=title) | Event | [Author](?order=desc&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 | +// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 | +// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 | +// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 | +// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 | +// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 | +// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 | +// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 | +// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 | +// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 | +// **1** | [2](?page=2) +// +// +------------------------------- +// | PATH: ?order=asc&sort=author +// | RESULT: +// # Presentations +// | [Date](?order=desc&sort=date) | [Title](?order=desc&sort=title) | Event | [Author](?order=desc&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 | +// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 | +// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 | +// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 | +// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 | +// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 | +// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 | +// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 | +// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 | +// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 | +// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 | +// +// +// +------------------------------- +// | PATH: ?order=asc&sort=title&page=2 +// | RESULT: +// # Presentations +// | [Date](?order=desc&page=2&sort=date) | [Title](?order=desc&page=2&sort=title) | Event | [Author](?order=desc&page=2&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 | +// [1](?page=1) | **2** +// +// +------------------------------- +// | PATH: s15 +// | RESULT: +// # title15 +// **event2**, 15 Feb 2024 +// +// by **author1** +// +// [View as slides](/r/moul/present:s15/slides) +// +// --- +// ## s15.0 +// ## s15.1 +// ## s15.2 +// +// --- +// Last edited: 2009-02-13 23:31:30 +// +// Uploader: `g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm` +// +// Number of slides: 5 +// +// [Edit](/r/moul/present$help&func=Set&author=author1&date=2024-02-15&event=event2&slug=s15&title=title15) | [Delete](/r/moul/present$help&func=Delete&slug=s15) +// +// +// +// +------------------------------- +// | PATH: s15/slides +// | RESULT: +// # title15 +// **event2**, 15 Feb 2024 +// +// by **author1** +// +// | 1/5 | [Next →](/r/moul/present:s15/slides?page=2) +// +// +// +// +------------------------------- +// | PATH: s15/slides?page=2 +// | RESULT: +// # title15 +// ## s15.0 +// +// [← Prev](/r/moul/present:s15/slides?page=1) | 2/5 | [Next →](/r/moul/present:s15/slides?page=3) +// +// +// +// +------------------------------- +// | PATH: s15/slides?page=3 +// | RESULT: +// # title15 +// ## s15.1 +// +// [← Prev](/r/moul/present:s15/slides?page=2) | 3/5 | [Next →](/r/moul/present:s15/slides?page=4) +// +// +// +// +------------------------------- +// | PATH: s15/slides?page=4 +// | RESULT: +// # title15 +// ## s15.2 +// +// [← Prev](/r/moul/present:s15/slides?page=3) | 4/5 | [Next →](/r/moul/present:s15/slides?page=5) +// +// +// +// +------------------------------- +// | PATH: s15/slides?page=5 +// | RESULT: +// # title15 +// ## Thank You! +// author1 +// +// 🔗 [https://tests\.gno\.land/r/moul/present:s15](https://tests.gno.land/r/moul/present:s15) +// +// [← Prev](/r/moul/present:s15/slides?page=4) | 5/5 | +// +// +// diff --git a/examples/gno.land/r/moul/present/present_init.gno b/examples/gno.land/r/moul/present/present_init.gno new file mode 100644 index 00000000000..b103bdf8cd6 --- /dev/null +++ b/examples/gno.land/r/moul/present/present_init.gno @@ -0,0 +1,25 @@ +package present + +func init() { + _ = Set( + "demo", // id + "Demo Slides", // title + "Demo Event", // event + "@demo", // author + "2025-02-02", // date + `## Slide One +- Point A +- Point B +- Point C + +## Slide Two +- Feature 1 +- Feature 2 +- Feature 3 + +## Slide Three +- Next step +- Another step +- Final step`, + ) +} diff --git a/examples/gno.land/r/moul/present/present_miami23.gno b/examples/gno.land/r/moul/present/present_miami23.gno deleted file mode 100644 index ca2160de3a9..00000000000 --- a/examples/gno.land/r/moul/present/present_miami23.gno +++ /dev/null @@ -1,42 +0,0 @@ -package present - -func init() { - path := "miami23" - title := "Portal Loop Demo (Miami 2023)" - body := ` -Rendered by Gno. - -[Source (WIP)](https://github.com/gnolang/gno/pull/1176) - -## Portal Loop - -- DONE: Dynamic homepage, key pages, aliases, and redirects. -- TODO: Deploy with history, complete worxdao v0. -- Will replace the static gno.land site. -- Enhances local development. - -[GitHub Issue](https://github.com/gnolang/gno/issues/1108) - -## Roadmap - -- Crafting the roadmap this week, open to collaboration. -- Combining onchain (portal loop) and offchain (GitHub). -- Next week: Unveiling the official v0 roadmap. - -## Teams, DAOs, Projects - -- Developing worxDAO contracts for directories of projects and teams. -- GitHub teams and projects align with this structure. -- CODEOWNER file updates coming. -- Initial teams announced next week. - -## Tech Team Retreat Plan - -- Continue Portal Loop. -- Consider dApp development. -- Explore new topics [here](https://github.com/orgs/gnolang/projects/15/). -- Engage in workshops. -- Connect and have fun with colleagues. -` - _ = b.NewPost(adminAddr, path, title, body, "2023-10-15T13:17:24Z", []string{"moul"}, []string{"demo", "portal-loop", "miami"}) -} diff --git a/examples/gno.land/r/moul/present/present_miami23_filetest.gno b/examples/gno.land/r/moul/present/present_miami23_filetest.gno deleted file mode 100644 index 09d332ec6e4..00000000000 --- a/examples/gno.land/r/moul/present/present_miami23_filetest.gno +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "gno.land/r/moul/present" -) - -func main() { - println(present.Render("")) - println("------------------------------------") - println(present.Render("p/miami23")) -} diff --git a/examples/gno.land/r/moul/present/presentations.gno b/examples/gno.land/r/moul/present/presentations.gno deleted file mode 100644 index c5529804751..00000000000 --- a/examples/gno.land/r/moul/present/presentations.gno +++ /dev/null @@ -1,17 +0,0 @@ -package present - -import ( - "gno.land/p/demo/blog" -) - -// TODO: switch from p/blog to p/present - -var b = &blog.Blog{ - Title: "Manfred's Presentations", - Prefix: "/r/moul/present:", - NoBreadcrumb: true, -} - -func Render(path string) string { - return b.Render(path) -} From 6fe3b319ff45f97ac4912ddecc530addaecb58c7 Mon Sep 17 00:00:00 2001 From: Miguel Victoria Villaquiran Date: Tue, 4 Feb 2025 23:03:49 +0100 Subject: [PATCH 23/60] chore(pipeline): run build of github pages on pullRequests (#3607) Co-authored-by: Morgan Bazalgette --- .github/workflows/gh-pages.yml | 23 ++++++++++++++++++----- misc/stdlib_diff/Makefile | 4 +++- misc/stdlib_diff/README.md | 4 ++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index a293469bb5d..2b27a2537e1 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,11 +1,14 @@ # generate Go docs and publish on gh-pages branch # Live at: https://gnolang.github.io/gno -name: Go Reference Docs Deployment +name: GitHub pages (godoc & stdlib_diff) build and deploy on: push: branches: - master + pull_request: + branches: + - master workflow_dispatch: permissions: @@ -19,29 +22,39 @@ concurrency: jobs: build: - if: ${{ github.repository == 'gnolang/gno' }} # Alternatively, validate based on provided tokens and permissions. + if: github.repository == 'gnolang/gno' # Alternatively, validate based on provided tokens and permissions. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - - run: echo "GOROOT=$(go env GOROOT)" >> $GITHUB_ENV - - run: echo $GOROOT + # Use the goroot at the top of the project to compare with the GnoVM + # stdlib, rather than the one in stdlib_diff (which may have a go.mod with + # a different toolchain version). + - run: echo "GOROOT_SAVE=$(go env GOROOT)" >> $GITHUB_ENV - run: "cd misc/stdlib_diff && make gen" - run: "cd misc/gendocs && make install gen" - run: "mkdir -p pages_output/stdlib_diff" - run: | cp -r misc/gendocs/godoc/* pages_output/ cp -r misc/stdlib_diff/stdlib_diff/* pages_output/stdlib_diff/ + + # These two last steps will be skipped on pull requests - uses: actions/configure-pages@v5 id: pages + if: github.event_name != 'pull_request' + - uses: actions/upload-pages-artifact@v3 + if: github.event_name != 'pull_request' with: path: ./pages_output deploy: - if: ${{ github.repository == 'gnolang/gno' }} # Alternatively, validate based on provided tokens and permissions. + if: > + github.repository == 'gnolang/gno' && + github.ref == 'refs/heads/master' && + github.event_name == 'push' runs-on: ubuntu-latest environment: name: github-pages diff --git a/misc/stdlib_diff/Makefile b/misc/stdlib_diff/Makefile index 439af22c586..32dcf95a2ec 100644 --- a/misc/stdlib_diff/Makefile +++ b/misc/stdlib_diff/Makefile @@ -1,7 +1,9 @@ all: clean gen +GOROOT_SAVE ?= $(shell go env GOROOT) + gen: - go run . -src $(GOROOT)/src -dst ../../gnovm/stdlibs -out ./stdlib_diff + go run . -src $(GOROOT_SAVE)/src -dst ../../gnovm/stdlibs -out ./stdlib_diff clean: rm -rf stdlib_diff diff --git a/misc/stdlib_diff/README.md b/misc/stdlib_diff/README.md index 32c3cbcd93d..47d05a0373b 100644 --- a/misc/stdlib_diff/README.md +++ b/misc/stdlib_diff/README.md @@ -1,6 +1,6 @@ # stdlibs_diff -stdlibs_diff is a tool that generates an html report indicating differences between gno standard libraries and go standrad libraries +stdlibs_diff is a tool that generates an html report indicating differences between gno standard libraries and go standard libraries. ## Usage @@ -27,4 +27,4 @@ Compare the `gno` standard libraries the `go` standard libraries ## Tips -An index.html is generated at the root of the report location. Utilize it to navigate easily through the report. \ No newline at end of file +An index.html is generated at the root of the report location. Utilize it to navigate easily through the report. From d75f1a2fe060c64f0ed51182f5dda965043ace9f Mon Sep 17 00:00:00 2001 From: Nemanja Matic <106317308+Nemanya8@users.noreply.github.com> Date: Tue, 4 Feb 2025 17:38:43 -0500 Subject: [PATCH 24/60] fix(docs/gnokey): addpkg subcommand documentation (#3635) --- docs/gno-tooling/cli/gnokey/state-changing-calls.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/gno-tooling/cli/gnokey/state-changing-calls.md b/docs/gno-tooling/cli/gnokey/state-changing-calls.md index 79a777cca51..b301e99be56 100644 --- a/docs/gno-tooling/cli/gnokey/state-changing-calls.md +++ b/docs/gno-tooling/cli/gnokey/state-changing-calls.md @@ -99,12 +99,11 @@ Next, let's configure the `addpkg` subcommand to publish this package to the the `example/p/` folder, the command will look like this: ```bash -gnokey maketx addpkg \ +gnokey maketx addpkg \ -pkgpath "gno.land/p//hello_world" \ -pkgdir "." \ --send "" \ -gas-fee 10000000ugnot \ --gas-wanted 8000000 \ +-gas-wanted 200000 \ -broadcast \ -chainid portal-loop \ -remote "https://rpc.gno.land:443" @@ -114,15 +113,14 @@ Once we have added a desired [namespace](../../../concepts/namespaces.md) to upl a keypair name to use to execute the transaction: ```bash -gnokey maketx addpkg \ +gnokey maketx addpkg \ -pkgpath "gno.land/p/examplenamespace/hello_world" \ -pkgdir "." \ --send "" \ -gas-fee 10000000ugnot \ -gas-wanted 200000 \ -broadcast \ -chainid portal-loop \ --remote "https://rpc.gno.land:443" +-remote "https://rpc.gno.land:443" \ mykey ``` From d7bfee2a5946446dccd8f00812b29daa4ca7a437 Mon Sep 17 00:00:00 2001 From: Mustapha <102119509+mous1985@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:28:26 +0100 Subject: [PATCH 25/60] feat(mux): add wildcard (*) for more flexible route matching (#3631) This pull request is for add some TODOs in `p/demo/mux` : - Add handling for NotFoundHandler - Add wildcard detection in route Basic example: ```go router.HandleFunc("r/user/*", func(rw *ResponseWriter, req *Request)) // match: // "r/user/profile" // "r/user/posts" // "r/user/home/avatar" --- examples/gno.land/p/demo/mux/handler.gno | 6 ++-- examples/gno.land/p/demo/mux/request.gno | 27 ++++++++------ examples/gno.land/p/demo/mux/request_test.gno | 33 ++++++++++------- examples/gno.land/p/demo/mux/router.gno | 35 ++++++++++++++++--- examples/gno.land/p/demo/mux/router_test.gno | 28 ++++++++++++++- 5 files changed, 98 insertions(+), 31 deletions(-) diff --git a/examples/gno.land/p/demo/mux/handler.gno b/examples/gno.land/p/demo/mux/handler.gno index 835d050a52c..4d937dbacab 100644 --- a/examples/gno.land/p/demo/mux/handler.gno +++ b/examples/gno.land/p/demo/mux/handler.gno @@ -7,6 +7,8 @@ type Handler struct { type HandlerFunc func(*ResponseWriter, *Request) -// TODO: type ErrHandlerFunc func(*ResponseWriter, *Request) error -// TODO: NotFoundHandler +type ErrHandlerFunc func(*ResponseWriter, *Request) error + +type NotFoundHandler func(*ResponseWriter, *Request) + // TODO: AutomaticIndex diff --git a/examples/gno.land/p/demo/mux/request.gno b/examples/gno.land/p/demo/mux/request.gno index 7b5b74da91b..eaa2f287069 100644 --- a/examples/gno.land/p/demo/mux/request.gno +++ b/examples/gno.land/p/demo/mux/request.gno @@ -18,24 +18,29 @@ type Request struct { // GetVar retrieves a variable from the path based on routing rules. func (r *Request) GetVar(key string) string { - var ( - handlerParts = strings.Split(r.HandlerPath, "/") - reqParts = strings.Split(r.Path, "/") - ) - - for i := 0; i < len(handlerParts); i++ { - handlerPart := handlerParts[i] + handlerParts := strings.Split(r.HandlerPath, "/") + reqParts := strings.Split(r.Path, "/") + reqIndex := 0 + for handlerIndex := 0; handlerIndex < len(handlerParts); handlerIndex++ { + handlerPart := handlerParts[handlerIndex] switch { case handlerPart == "*": - // XXX: implement a/b/*/d/e - panic("not implemented") + // If a wildcard "*" is found, consume all remaining segments + wildcardParts := reqParts[reqIndex:] + reqIndex = len(reqParts) // Consume all remaining segments + return strings.Join(wildcardParts, "/") // Return all remaining segments as a string case strings.HasPrefix(handlerPart, "{") && strings.HasSuffix(handlerPart, "}"): + // If a variable of the form {param} is found we compare it with the key parameter := handlerPart[1 : len(handlerPart)-1] if parameter == key { - return reqParts[i] + return reqParts[reqIndex] } + reqIndex++ default: - // continue + if reqIndex >= len(reqParts) || handlerPart != reqParts[reqIndex] { + return "" + } + reqIndex++ } } diff --git a/examples/gno.land/p/demo/mux/request_test.gno b/examples/gno.land/p/demo/mux/request_test.gno index 5f8088b4964..24c611c1f9d 100644 --- a/examples/gno.land/p/demo/mux/request_test.gno +++ b/examples/gno.land/p/demo/mux/request_test.gno @@ -1,8 +1,10 @@ package mux import ( - "fmt" "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" ) func TestRequest_GetVar(t *testing.T) { @@ -12,28 +14,35 @@ func TestRequest_GetVar(t *testing.T) { getVarKey string expectedOutput string }{ + {"users/{id}", "users/123", "id", "123"}, {"users/123", "users/123", "id", ""}, {"users/{id}", "users/123", "nonexistent", ""}, - {"a/{b}/c/{d}", "a/42/c/1337", "b", "42"}, - {"a/{b}/c/{d}", "a/42/c/1337", "d", "1337"}, - {"{a}", "foo", "a", "foo"}, - // TODO: wildcards: a/*/c - // TODO: multiple patterns per slashes: a/{b}-{c}/d - } + {"users/{userId}/posts/{postId}", "users/123/posts/456", "userId", "123"}, + {"users/{userId}/posts/{postId}", "users/123/posts/456", "postId", "456"}, + + // Wildcards + {"*", "users/123", "*", "users/123"}, + {"*", "users/123/posts/456", "*", "users/123/posts/456"}, + {"*", "users/123/posts/456/comments/789", "*", "users/123/posts/456/comments/789"}, + {"users/*", "users/john/posts", "*", "john/posts"}, + {"users/*/comments", "users/jane/comments", "*", "jane/comments"}, + {"api/*/posts/*", "api/v1/posts/123", "*", "v1/posts/123"}, + // wildcards and parameters + {"api/{version}/*", "api/v1/user/settings", "version", "v1"}, + } for _, tt := range cases { - name := fmt.Sprintf("%s-%s", tt.handlerPath, tt.reqPath) + name := ufmt.Sprintf("%s-%s", tt.handlerPath, tt.reqPath) t.Run(name, func(t *testing.T) { req := &Request{ HandlerPath: tt.handlerPath, Path: tt.reqPath, } - output := req.GetVar(tt.getVarKey) - if output != tt.expectedOutput { - t.Errorf("Expected '%q, but got %q", tt.expectedOutput, output) - } + uassert.Equal(t, tt.expectedOutput, output, + "handler: %q, path: %q, key: %q", + tt.handlerPath, tt.reqPath, tt.getVarKey) }) } } diff --git a/examples/gno.land/p/demo/mux/router.gno b/examples/gno.land/p/demo/mux/router.gno index fe6bf70abdf..4fca43a0378 100644 --- a/examples/gno.land/p/demo/mux/router.gno +++ b/examples/gno.land/p/demo/mux/router.gno @@ -5,7 +5,7 @@ import "strings" // Router handles the routing and rendering logic. type Router struct { routes []Handler - NotFoundHandler HandlerFunc + NotFoundHandler NotFoundHandler } // NewRouter creates a new Router instance. @@ -23,8 +23,14 @@ func (r *Router) Render(reqPath string) string { for _, route := range r.routes { patParts := strings.Split(route.Pattern, "/") - - if len(patParts) != len(reqParts) { + wildcard := false + for _, part := range patParts { + if part == "*" { + wildcard = true + break + } + } + if !wildcard && len(patParts) != len(reqParts) { continue } @@ -34,7 +40,7 @@ func (r *Router) Render(reqPath string) string { reqPart := reqParts[i] if patPart == "*" { - continue + break } if strings.HasPrefix(patPart, "{") && strings.HasSuffix(patPart, "}") { continue @@ -63,12 +69,31 @@ func (r *Router) Render(reqPath string) string { return res.Output() } -// Handle registers a route and its handler function. +// HandleFunc registers a route and its handler function. func (r *Router) HandleFunc(pattern string, fn HandlerFunc) { route := Handler{Pattern: pattern, Fn: fn} r.routes = append(r.routes, route) } +// HandleErrFunc registers a route and its error handler function. +func (r *Router) HandleErrFunc(pattern string, fn ErrHandlerFunc) { + + // Convert ErrHandlerFunc to regular HandlerFunc + handler := func(res *ResponseWriter, req *Request) { + if err := fn(res, req); err != nil { + res.Write("Error: " + err.Error()) + } + } + + r.HandleFunc(pattern, handler) +} + +// SetNotFoundHandler sets custom message for 404 defaultNotFoundHandler. +func (r *Router) SetNotFoundHandler(handler NotFoundHandler) { + r.NotFoundHandler = handler +} + +// stripQueryString removes query string from the request path. func stripQueryString(reqPath string) string { i := strings.Index(reqPath, "?") if i == -1 { diff --git a/examples/gno.land/p/demo/mux/router_test.gno b/examples/gno.land/p/demo/mux/router_test.gno index cc6aad62146..c1c5d218165 100644 --- a/examples/gno.land/p/demo/mux/router_test.gno +++ b/examples/gno.land/p/demo/mux/router_test.gno @@ -72,7 +72,33 @@ func TestRouter_Render(t *testing.T) { }) }, }, - + { + label: "wildcard in route", + path: "hello/Alice/Bob", + expectedOutput: "Matched: Alice/Bob", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/*", func(rw *ResponseWriter, req *Request) { + path := req.GetVar("*") + uassert.Equal(t, "Alice/Bob", path) + uassert.Equal(t, "hello/Alice/Bob", req.Path) + rw.Write("Matched: " + path) + }) + }, + }, + { + label: "wildcard in route with query string", + path: "hello/Alice/Bob?foo=bar", + expectedOutput: "Matched: Alice/Bob", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/*", func(rw *ResponseWriter, req *Request) { + path := req.GetVar("*") + uassert.Equal(t, "Alice/Bob", path) + uassert.Equal(t, "hello/Alice/Bob?foo=bar", req.RawPath) + uassert.Equal(t, "hello/Alice/Bob", req.Path) + rw.Write("Matched: " + path) + }) + }, + }, // TODO: {"hello", "Hello, world!"}, // TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc } From e12b3f1a352556259dc49ee585cc78cfbddf2bed Mon Sep 17 00:00:00 2001 From: JJOptimist <86833563+JJOptimist@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:32:00 +0100 Subject: [PATCH 26/60] feat: add JJOptimist's Homepage realm to examples (#3405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request adds a new realm example to the Gno examples repository—JJOptimist's Homepage. The realm includes: - Personal introduction and contact information - Configuration management system - Owner controls - Tests for ownership and rendering --------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../gno.land/r/jjoptimist/home/config.gno | 32 ++++++++ examples/gno.land/r/jjoptimist/home/gno.mod | 1 + examples/gno.land/r/jjoptimist/home/home.gno | 82 +++++++++++++++++++ .../gno.land/r/jjoptimist/home/home_test.gno | 60 ++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 examples/gno.land/r/jjoptimist/home/config.gno create mode 100644 examples/gno.land/r/jjoptimist/home/gno.mod create mode 100644 examples/gno.land/r/jjoptimist/home/home.gno create mode 100644 examples/gno.land/r/jjoptimist/home/home_test.gno diff --git a/examples/gno.land/r/jjoptimist/home/config.gno b/examples/gno.land/r/jjoptimist/home/config.gno new file mode 100644 index 00000000000..7f6ad955806 --- /dev/null +++ b/examples/gno.land/r/jjoptimist/home/config.gno @@ -0,0 +1,32 @@ +package home + +import ( + "std" + + "gno.land/p/demo/ownable" +) + +type Config struct { + Title string + Description string + Github string +} + +var config = Config{ + Title: "JJOptimist's Home Realm 🏠", + Description: "Exploring Gno and building on-chain", + Github: "jjoptimist", +} + +var Ownable = ownable.NewWithAddress(std.Address("g16vfw3r7zuz43fhky3xfsuc2hdv9tnhvlkyn0nj")) + +func GetConfig() Config { + return config +} + +func UpdateConfig(newTitle, newDescription, newGithub string) { + Ownable.AssertCallerIsOwner() + config.Title = newTitle + config.Description = newDescription + config.Github = newGithub +} diff --git a/examples/gno.land/r/jjoptimist/home/gno.mod b/examples/gno.land/r/jjoptimist/home/gno.mod new file mode 100644 index 00000000000..b4b591f6ab7 --- /dev/null +++ b/examples/gno.land/r/jjoptimist/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/jjoptimist/home diff --git a/examples/gno.land/r/jjoptimist/home/home.gno b/examples/gno.land/r/jjoptimist/home/home.gno new file mode 100644 index 00000000000..91a23670271 --- /dev/null +++ b/examples/gno.land/r/jjoptimist/home/home.gno @@ -0,0 +1,82 @@ +package home + +import ( + "std" + "strconv" + "time" + + "gno.land/r/leon/hof" +) + +const ( + gnomeArt1 = ` /\ + / \ + ,,,,, +(o.o) +(\_/) +-"-"-` + + gnomeArt2 = ` /\ + / \ + ,,,,, +(^.^) +(\_/) + -"-` + + gnomeArt3 = ` /\ + / \ + ,,,,, +(*.*) +(\_/) +"-"-"` + + gnomeArt4 = ` /\ + / \ + ,,,,, +(o.~) +(\_/) + -"-` +) + +var creation time.Time + +func getGnomeArt(height int64) string { + var art string + switch { + case height%7 == 0: + art = gnomeArt4 // winking gnome + case height%5 == 0: + art = gnomeArt3 // starry-eyed gnome + case height%3 == 0: + art = gnomeArt2 // happy gnome + default: + art = gnomeArt1 // regular gnome + } + return "```\n" + art + "\n```\n" +} + +func init() { + creation = time.Now() + hof.Register() +} + +func Render(path string) string { + height := std.GetHeight() + + output := "# " + config.Title + "\n\n" + + output += "## About Me\n" + output += "- 👋 Hi, I'm JJOptimist\n" + output += getGnomeArt(height) + output += "- 🌱 " + config.Description + "\n" + + output += "## Contact\n" + output += "- 📫 GitHub: [" + config.Github + "](https://github.com/" + config.Github + ")\n" + + output += "\n---\n" + output += "_Realm created: " + creation.Format("2006-01-02 15:04:05 UTC") + "_\n" + output += "_Owner: " + Ownable.Owner().String() + "_\n" + output += "_Current Block Height: " + strconv.Itoa(int(height)) + "_" + + return output +} diff --git a/examples/gno.land/r/jjoptimist/home/home_test.gno b/examples/gno.land/r/jjoptimist/home/home_test.gno new file mode 100644 index 00000000000..742204cca71 --- /dev/null +++ b/examples/gno.land/r/jjoptimist/home/home_test.gno @@ -0,0 +1,60 @@ +package home + +import ( + "strings" + "testing" +) + +func TestConfig(t *testing.T) { + cfg := GetConfig() + + if cfg.Title != "JJOptimist's Home Realm 🏠" { + t.Errorf("Expected title to be 'JJOptimist's Home Realm 🏠', got %s", cfg.Title) + } + if cfg.Description != "Exploring Gno and building on-chain" { + t.Errorf("Expected description to be 'Exploring Gno and building on-chain', got %s", cfg.Description) + } + if cfg.Github != "jjoptimist" { + t.Errorf("Expected github to be 'jjoptimist', got %s", cfg.Github) + } +} + +func TestRender(t *testing.T) { + output := Render("") + + // Test that required sections are present + if !strings.Contains(output, "# "+config.Title) { + t.Error("Rendered output missing title") + } + if !strings.Contains(output, "## About Me") { + t.Error("Rendered output missing About Me section") + } + if !strings.Contains(output, "## Contact") { + t.Error("Rendered output missing Contact section") + } + if !strings.Contains(output, config.Description) { + t.Error("Rendered output missing description") + } + if !strings.Contains(output, config.Github) { + t.Error("Rendered output missing github link") + } +} + +func TestGetGnomeArt(t *testing.T) { + tests := []struct { + height int64 + expected string + }{ + {7, gnomeArt4}, // height divisible by 7 + {5, gnomeArt3}, // height divisible by 5 + {3, gnomeArt2}, // height divisible by 3 + {2, gnomeArt1}, // default case + } + + for _, tt := range tests { + art := getGnomeArt(tt.height) + if !strings.Contains(art, tt.expected) { + t.Errorf("For height %d, expected art containing %s, got %s", tt.height, tt.expected, art) + } + } +} From 9e6a67bc0709a6a4a3186b11bb1b9bffc6200a04 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:21:35 +0100 Subject: [PATCH 27/60] fix(p2p): Avoid inifinty loop during transport/listener `Accept` (#3662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR aims to fix two potential infinite loops during `Accept` in the `p2p` package. - First, it removes the `ErrTransportInactive` error. Based on how `Accept` is used, I believe the inactive error does not make much sense here. `Accept` should always block, even in an inactive state. Having `Accept` in two states—blocking and non-blocking—complicates the logic unnecessarily. Additionally, returning an inactive error creates an infinite loop, as the handler will keep looping over `Accept` until a blocking state is reached. - If the underlying transport or listener is closed, the context should also be canceled, as this is not a recoverable error. Currently, if the transport or listener is closed, it will loop indefinitely until the context is properly closed. While this situation should rarely occur, since the context should be canceled when closing the transport, I believe it is safe to force the cancellation of the context to avoid any potential deadloop. **EDIT:** I've removed the context select from the loop to simplify the logic. The logic remains unchanged, but it avoids having two sources of truth. This PR also reduces unnecessary error and warning noise when the listener or transport is closed. --------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- tm2/pkg/p2p/switch.go | 73 ++++++++++++------------ tm2/pkg/p2p/switch_test.go | 31 +++++++++++ tm2/pkg/p2p/transport.go | 102 +++++++++++++++------------------- tm2/pkg/p2p/transport_test.go | 10 ++-- tm2/pkg/service/service.go | 2 +- 5 files changed, 117 insertions(+), 101 deletions(-) diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index 7d9e768dd4b..7784c1f3989 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -5,6 +5,7 @@ import ( "context" "crypto/rand" "encoding/binary" + "errors" "fmt" "math" "sync" @@ -622,50 +623,50 @@ func (sw *MultiplexSwitch) isPrivatePeer(id types.ID) bool { // and persisting them func (sw *MultiplexSwitch) runAcceptLoop(ctx context.Context) { for { - select { - case <-ctx.Done(): - sw.Logger.Debug("switch context close received") + p, err := sw.transport.Accept(ctx, sw.peerBehavior) - return + switch { + case err == nil: // ok + case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded): + // Upper context as been canceled/timeout + sw.Logger.Debug("switch context close received") + return // exit + case errors.As(err, &errTransportClosed): + // Underlaying transport as been closed + sw.Logger.Warn("cannot accept connection on closed transport, exiting") + return // exit default: - p, err := sw.transport.Accept(ctx, sw.peerBehavior) - if err != nil { - sw.Logger.Error( - "error encountered during peer connection accept", - "err", err, - ) + // An error occurred during accept, report and continue + sw.Logger.Error("error encountered during peer connection accept", "err", err) + continue + } - continue - } + // Ignore connection if we already have enough peers. + if in := sw.Peers().NumInbound(); in >= sw.maxInboundPeers { + sw.Logger.Info( + "Ignoring inbound connection: already have enough inbound peers", + "address", p.SocketAddr(), + "have", in, + "max", sw.maxInboundPeers, + ) - // Ignore connection if we already have enough peers. - if in := sw.Peers().NumInbound(); in >= sw.maxInboundPeers { - sw.Logger.Info( - "Ignoring inbound connection: already have enough inbound peers", - "address", p.SocketAddr(), - "have", in, - "max", sw.maxInboundPeers, - ) + sw.transport.Remove(p) + continue + } - sw.transport.Remove(p) + // There are open peer slots, add peers + if err := sw.addPeer(p); err != nil { + sw.transport.Remove(p) - continue + if p.IsRunning() { + _ = p.Stop() } - // There are open peer slots, add peers - if err := sw.addPeer(p); err != nil { - sw.transport.Remove(p) - - if p.IsRunning() { - _ = p.Stop() - } - - sw.Logger.Info( - "Ignoring inbound connection: error while adding peer", - "err", err, - "id", p.ID(), - ) - } + sw.Logger.Info( + "Ignoring inbound connection: error while adding peer", + "err", err, + "id", p.ID(), + ) } } } diff --git a/tm2/pkg/p2p/switch_test.go b/tm2/pkg/p2p/switch_test.go index cf0a0c41bb5..b10ab3faba5 100644 --- a/tm2/pkg/p2p/switch_test.go +++ b/tm2/pkg/p2p/switch_test.go @@ -890,3 +890,34 @@ func TestCalculateBackoff(t *testing.T) { } }) } + +func TestSwitchAcceptLoopTransportClosed(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var transportClosed bool + mockTransport := &mockTransport{ + acceptFn: func(context.Context, PeerBehavior) (PeerConn, error) { + transportClosed = true + return nil, errTransportClosed + }, + } + + sw := NewMultiplexSwitch(mockTransport) + + // Run the accept loop + done := make(chan struct{}) + go func() { + sw.runAcceptLoop(ctx) + close(done) // signal that accept loop as ended + }() + + select { + case <-time.After(time.Second * 2): + require.FailNow(t, "timeout while waiting for running loop to stop") + case <-done: + assert.True(t, transportClosed) + } +} diff --git a/tm2/pkg/p2p/transport.go b/tm2/pkg/p2p/transport.go index 150072ad5eb..255fa60152b 100644 --- a/tm2/pkg/p2p/transport.go +++ b/tm2/pkg/p2p/transport.go @@ -2,6 +2,7 @@ package p2p import ( "context" + goerrors "errors" "fmt" "io" "log/slog" @@ -22,7 +23,6 @@ const defaultHandshakeTimeout = 3 * time.Second var ( errTransportClosed = errors.New("transport is closed") - errTransportInactive = errors.New("transport is inactive") errDuplicateConnection = errors.New("duplicate peer connection") errPeerIDNodeInfoMismatch = errors.New("connection ID does not match node info ID") errPeerIDDialMismatch = errors.New("connection ID does not match dialed ID") @@ -75,7 +75,10 @@ func NewMultiplexTransport( mConfig conn.MConnConfig, logger *slog.Logger, ) *MultiplexTransport { + ctx, cancel := context.WithCancel(context.Background()) return &MultiplexTransport{ + ctx: ctx, + cancelFn: cancel, peerCh: make(chan peerInfo, 1), mConfig: mConfig, nodeInfo: nodeInfo, @@ -92,12 +95,6 @@ func (mt *MultiplexTransport) NetAddress() types.NetAddress { // Accept waits for a verified inbound Peer to connect, and returns it [BLOCKING] func (mt *MultiplexTransport) Accept(ctx context.Context, behavior PeerBehavior) (PeerConn, error) { - // Sanity check, no need to wait - // on an inactive transport - if mt.listener == nil { - return nil, errTransportInactive - } - select { case <-ctx.Done(): return nil, ctx.Err() @@ -142,44 +139,35 @@ func (mt *MultiplexTransport) Close() error { } mt.cancelFn() - return mt.listener.Close() } // Listen starts an active process of listening for incoming connections [NON-BLOCKING] -func (mt *MultiplexTransport) Listen(addr types.NetAddress) (rerr error) { +func (mt *MultiplexTransport) Listen(addr types.NetAddress) error { // Reserve a port, and start listening ln, err := net.Listen("tcp", addr.DialString()) if err != nil { return fmt.Errorf("unable to listen on address, %w", err) } - defer func() { - if rerr != nil { - ln.Close() - } - }() - if addr.Port == 0 { // net.Listen on port 0 means the kernel will auto-allocate a port // - find out which one has been given to us. tcpAddr, ok := ln.Addr().(*net.TCPAddr) if !ok { + ln.Close() return fmt.Errorf("error finding port (after listening on port 0): %w", err) } addr.Port = uint16(tcpAddr.Port) } - // Set up the context - mt.ctx, mt.cancelFn = context.WithCancel(context.Background()) - mt.netAddr = addr mt.listener = ln // Run the routine for accepting // incoming peer connections - go mt.runAcceptLoop() + go mt.runAcceptLoop(mt.ctx) return nil } @@ -189,60 +177,58 @@ func (mt *MultiplexTransport) Listen(addr types.NetAddress) (rerr error) { // 1. accepted by the transport // 2. filtered // 3. upgraded (handshaked + verified) -func (mt *MultiplexTransport) runAcceptLoop() { +func (mt *MultiplexTransport) runAcceptLoop(ctx context.Context) { var wg sync.WaitGroup - defer func() { wg.Wait() // Wait for all process routines - close(mt.peerCh) }() - for { - select { - case <-mt.ctx.Done(): - mt.logger.Debug("transport accept context closed") + ctx, cancel := context.WithCancel(ctx) + defer cancel() // cancel sub-connection process - return + for { + // Accept an incoming peer connection + c, err := mt.listener.Accept() + + switch { + case err == nil: // ok + case goerrors.Is(err, net.ErrClosed): + // Listener has been closed, this is not recoverable. + mt.logger.Debug("listener has been closed") + return // exit default: - // Accept an incoming peer connection - c, err := mt.listener.Accept() + // An error occurred during accept, report and continue + mt.logger.Warn("accept p2p connection error", "err", err) + continue + } + + // Process the new connection asynchronously + wg.Add(1) + + go func(c net.Conn) { + defer wg.Done() + + info, err := mt.processConn(c, "") if err != nil { mt.logger.Error( - "unable to accept p2p connection", + "unable to process p2p connection", "err", err, ) - continue - } - - // Process the new connection asynchronously - wg.Add(1) + // Close the connection + _ = c.Close() - go func(c net.Conn) { - defer wg.Done() - - info, err := mt.processConn(c, "") - if err != nil { - mt.logger.Error( - "unable to process p2p connection", - "err", err, - ) - - // Close the connection - _ = c.Close() - - return - } + return + } - select { - case mt.peerCh <- info: - case <-mt.ctx.Done(): - // Give up if the transport was closed. - _ = c.Close() - } - }(c) - } + select { + case mt.peerCh <- info: + case <-ctx.Done(): + // Give up if the transport was closed. + _ = c.Close() + } + }(c) } } diff --git a/tm2/pkg/p2p/transport_test.go b/tm2/pkg/p2p/transport_test.go index 3eb3264ec2b..5ec4efda1ad 100644 --- a/tm2/pkg/p2p/transport_test.go +++ b/tm2/pkg/p2p/transport_test.go @@ -122,14 +122,12 @@ func TestMultiplexTransport_Accept(t *testing.T) { transport := NewMultiplexTransport(ni, nk, mCfg, logger) - p, err := transport.Accept(context.Background(), nil) + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + p, err := transport.Accept(ctx, nil) assert.Nil(t, p) - assert.ErrorIs( - t, - err, - errTransportInactive, - ) + assert.ErrorIs(t, err, context.DeadlineExceeded) }) t.Run("transport closed", func(t *testing.T) { diff --git a/tm2/pkg/service/service.go b/tm2/pkg/service/service.go index 05f7a4f4ae6..c93eb06b298 100644 --- a/tm2/pkg/service/service.go +++ b/tm2/pkg/service/service.go @@ -159,7 +159,7 @@ func (bs *BaseService) OnStart() error { return nil } func (bs *BaseService) Stop() error { if atomic.CompareAndSwapUint32(&bs.stopped, 0, 1) { if atomic.LoadUint32(&bs.started) == 0 { - bs.Logger.Error(fmt.Sprintf("Not stopping %v -- have not been started yet", bs.name), "impl", bs.impl) + bs.Logger.Warn(fmt.Sprintf("Not stopping %v -- have not been started yet", bs.name), "impl", bs.impl) // revert flag atomic.StoreUint32(&bs.stopped, 0) return ErrNotStarted From 0b76b0b0dfe9ae6d0f6546f8142113310f51453b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaza=C3=AF?= <149690535+kazai777@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:02:35 +0100 Subject: [PATCH 28/60] feat(cmd/gno): add gno version command (#3002) I've added the `version` command to gno to get the version of gno that is installed
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
--------- Co-authored-by: Morgan --- gnovm/Makefile | 5 ++++- gnovm/cmd/gno/main.go | 1 + gnovm/cmd/gno/version.go | 24 ++++++++++++++++++++++++ gnovm/cmd/gno/version_test.go | 32 ++++++++++++++++++++++++++++++++ gnovm/pkg/version/version.go | 3 +++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 gnovm/cmd/gno/version.go create mode 100644 gnovm/cmd/gno/version_test.go create mode 100644 gnovm/pkg/version/version.go diff --git a/gnovm/Makefile b/gnovm/Makefile index ce745e44aae..2206fa2c8c8 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -27,11 +27,14 @@ GOTEST_FLAGS ?= -v -p 1 -timeout=30m GNOROOT_DIR ?= $(abspath $(lastword $(MAKEFILE_LIST))/../../) # We can't use '-trimpath' yet as amino use absolute path from call stack # to find some directory: see #1236 -GOBUILD_FLAGS ?= -ldflags "-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" +GOBUILD_FLAGS ?= -ldflags "-X github.com/gnolang/gno/gnovm/pkg/version.Version=$(VERSION) -X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" # file where to place cover profile; used for coverage commands which are # more complex than adding -coverprofile, like test.cmd.coverage. GOTEST_COVER_PROFILE ?= cmd-profile.out +# user for gno version [branch].[N]+[hash] +VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || echo "$(shell git rev-parse --abbrev-ref HEAD).$(shell git rev-list --count HEAD)+$(shell git rev-parse --short HEAD)") + ######################################## # Dev tools .PHONY: build diff --git a/gnovm/cmd/gno/main.go b/gnovm/cmd/gno/main.go index 5f8bb7b522e..b18e610d535 100644 --- a/gnovm/cmd/gno/main.go +++ b/gnovm/cmd/gno/main.go @@ -41,6 +41,7 @@ func newGnocliCmd(io commands.IO) *commands.Command { newTestCmd(io), newToolCmd(io), // version -- show cmd/gno, golang versions + newGnoVersionCmd(io), // vet ) diff --git a/gnovm/cmd/gno/version.go b/gnovm/cmd/gno/version.go new file mode 100644 index 00000000000..f9b967d1c40 --- /dev/null +++ b/gnovm/cmd/gno/version.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + + "github.com/gnolang/gno/gnovm/pkg/version" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// newVersionCmd creates a new version command +func newGnoVersionCmd(io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "version", + ShortUsage: "version", + ShortHelp: "display installed gno version", + }, + nil, + func(_ context.Context, args []string) error { + io.Println("gno version:", version.Version) + return nil + }, + ) +} diff --git a/gnovm/cmd/gno/version_test.go b/gnovm/cmd/gno/version_test.go new file mode 100644 index 00000000000..fab47319297 --- /dev/null +++ b/gnovm/cmd/gno/version_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "testing" + + "github.com/gnolang/gno/gnovm/pkg/version" +) + +func TestVersionApp(t *testing.T) { + originalVersion := version.Version + + t.Cleanup(func() { + version.Version = originalVersion + }) + + versionValues := []string{"chain/test4.2", "develop", "master"} + + testCases := make([]testMainCase, len(versionValues)) + for i, v := range versionValues { + testCases[i] = testMainCase{ + args: []string{"version"}, + stdoutShouldContain: "gno version: " + v, + } + } + + for i, testCase := range testCases { + t.Run(versionValues[i], func(t *testing.T) { + version.Version = versionValues[i] + testMainCaseRun(t, []testMainCase{testCase}) + }) + } +} diff --git a/gnovm/pkg/version/version.go b/gnovm/pkg/version/version.go new file mode 100644 index 00000000000..933d4fac3e5 --- /dev/null +++ b/gnovm/pkg/version/version.go @@ -0,0 +1,3 @@ +package version + +var Version = "develop" From 0bc4423841959df6dd3ef0db66ed2925f1acce26 Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Thu, 6 Feb 2025 22:44:39 +0900 Subject: [PATCH 29/60] fix(gnoweb): escape bash chars in help args (#3672) This PR ensures that special characters like ! and ? in user inputs are properly escaped when generating commands in the docs page ($help). Previously, entering ! or any other special char could cause the command string to break by omitting a closing ", making it invalid. This fix applies proper escaping to prevent such issues, ensuring that generated commands remain valid and executable. The fix introduces an escaping function that handles shell-sensitive characters before inserting them into the generated command strings. This approach ensures the commands remain intact without affecting their output when executed. Thus, the escape char is also removed from the cmd when the shell-sensitive char is removed from the arg input. cf: [issue 3355](https://github.com/gnolang/gno/issues/3355#issuecomment-2598841346) --- gno.land/pkg/gnoweb/components/layouts/header.html | 2 +- gno.land/pkg/gnoweb/frontend/js/realmhelp.ts | 9 +++++---- gno.land/pkg/gnoweb/frontend/js/utils.ts | 4 ++++ gno.land/pkg/gnoweb/public/js/realmhelp.js | 2 +- gno.land/pkg/gnoweb/public/js/utils.js | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/gno.land/pkg/gnoweb/components/layouts/header.html b/gno.land/pkg/gnoweb/components/layouts/header.html index 8a1433ccd1c..5743d0a82b6 100644 --- a/gno.land/pkg/gnoweb/components/layouts/header.html +++ b/gno.land/pkg/gnoweb/components/layouts/header.html @@ -21,4 +21,4 @@
{{ range .Links }} {{ template "ui/header_link" . }} {{ end }}
-{{ end }} +{{ end }} diff --git a/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts index 3177e034257..950c85cdbe3 100644 --- a/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts +++ b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts @@ -1,4 +1,4 @@ -import { debounce } from "./utils"; +import { debounce, escapeShellSpecialChars } from "./utils"; class Help { private DOM: { @@ -67,7 +67,7 @@ class Help { localStorage.setItem("helpAddressInput", address); this.funcList.forEach((func) => func.updateAddr(address)); - }); + }, 50); addressInput?.addEventListener("input", () => debouncedUpdate(addressInput)); cmdModeSelect?.addEventListener("change", (e) => { @@ -124,7 +124,7 @@ class HelpFunc { private bindEvents(): void { const debouncedUpdate = debounce((paramName: string, paramValue: string) => { if (paramName) this.updateArg(paramName, paramValue); - }); + }, 50); this.DOM.el.addEventListener("input", (e) => { const target = e.target as HTMLInputElement; @@ -143,10 +143,11 @@ class HelpFunc { } public updateArg(paramName: string, paramValue: string): void { + const escapedValue = escapeShellSpecialChars(paramValue); this.DOM.args .filter((arg) => arg.dataset.arg === paramName) .forEach((arg) => { - arg.textContent = paramValue || ""; + arg.textContent = escapedValue || ""; }); } diff --git a/gno.land/pkg/gnoweb/frontend/js/utils.ts b/gno.land/pkg/gnoweb/frontend/js/utils.ts index 83de509efa5..d975b4516f3 100644 --- a/gno.land/pkg/gnoweb/frontend/js/utils.ts +++ b/gno.land/pkg/gnoweb/frontend/js/utils.ts @@ -10,3 +10,7 @@ export function debounce void>(func: T, delay: num }, delay); }; } + +export function escapeShellSpecialChars(arg: string): string { + return arg.replace(/([$`"\\!|&;<>*?{}()])/g, "\\$1"); +} diff --git a/gno.land/pkg/gnoweb/public/js/realmhelp.js b/gno.land/pkg/gnoweb/public/js/realmhelp.js index 68bcafbb75f..7008a54514e 100644 --- a/gno.land/pkg/gnoweb/public/js/realmhelp.js +++ b/gno.land/pkg/gnoweb/public/js/realmhelp.js @@ -1 +1 @@ -function d(a,e=250){let t;return function(...s){t!==void 0&&clearTimeout(t),t=setTimeout(()=>{a.apply(this,s)},e)}}var l=class a{DOM;funcList;static SELECTORS={container:".js-help-view",func:"[data-func]",addressInput:"[data-role='help-input-addr']",cmdModeSelect:"[data-role='help-select-mode']"};constructor(){this.DOM={el:document.querySelector(a.SELECTORS.container),funcs:[],addressInput:null,cmdModeSelect:null},this.funcList=[],this.DOM.el?this.init():console.warn("Help: Main container not found.")}init(){let{el:e}=this.DOM;e&&(this.DOM.funcs=Array.from(e.querySelectorAll(a.SELECTORS.func)),this.DOM.addressInput=e.querySelector(a.SELECTORS.addressInput),this.DOM.cmdModeSelect=e.querySelector(a.SELECTORS.cmdModeSelect),this.funcList=this.DOM.funcs.map(t=>new o(t)),this.restoreAddress(),this.bindEvents())}restoreAddress(){let{addressInput:e}=this.DOM;if(e){let t=localStorage.getItem("helpAddressInput");t&&(e.value=t,this.funcList.forEach(s=>s.updateAddr(t)))}}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM,s=d(r=>{let n=r.value;localStorage.setItem("helpAddressInput",n),this.funcList.forEach(i=>i.updateAddr(n))});e?.addEventListener("input",()=>s(e)),t?.addEventListener("change",r=>{let n=r.target;this.funcList.forEach(i=>i.updateMode(n.value))})}},o=class a{DOM;funcName;static SELECTORS={address:"[data-role='help-code-address']",args:"[data-role='help-code-args']",mode:"[data-code-mode]",paramInput:"[data-role='help-param-input']"};constructor(e){this.DOM={el:e,addrs:Array.from(e.querySelectorAll(a.SELECTORS.address)),args:Array.from(e.querySelectorAll(a.SELECTORS.args)),modes:Array.from(e.querySelectorAll(a.SELECTORS.mode)),paramInputs:Array.from(e.querySelectorAll(a.SELECTORS.paramInput))},this.funcName=e.dataset.func||null,this.initializeArgs(),this.bindEvents()}static sanitizeArgsInput(e){let t=e.dataset.param||"",s=e.value.trim();return t||console.warn("sanitizeArgsInput: param is missing in arg input dataset."),{paramName:t,paramValue:s}}bindEvents(){let e=d((t,s)=>{t&&this.updateArg(t,s)});this.DOM.el.addEventListener("input",t=>{let s=t.target;if(s.dataset.role==="help-param-input"){let{paramName:r,paramValue:n}=a.sanitizeArgsInput(s);e(r,n)}})}initializeArgs(){this.DOM.paramInputs.forEach(e=>{let{paramName:t,paramValue:s}=a.sanitizeArgsInput(e);t&&this.updateArg(t,s)})}updateArg(e,t){this.DOM.args.filter(s=>s.dataset.arg===e).forEach(s=>{s.textContent=t||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let s=t.dataset.codeMode===e;t.classList.toggle("inline",s),t.classList.toggle("hidden",!s),t.dataset.copyContent=s?`help-cmd-${this.funcName}`:""})}},p=()=>new l;export{p as default}; +function d(s,e=250){let t;return function(...a){t!==void 0&&clearTimeout(t),t=setTimeout(()=>{s.apply(this,a)},e)}}function u(s){return s.replace(/([$`"\\!|&;<>*?{}()])/g,"\\$1")}var l=class s{DOM;funcList;static SELECTORS={container:".js-help-view",func:"[data-func]",addressInput:"[data-role='help-input-addr']",cmdModeSelect:"[data-role='help-select-mode']"};constructor(){this.DOM={el:document.querySelector(s.SELECTORS.container),funcs:[],addressInput:null,cmdModeSelect:null},this.funcList=[],this.DOM.el?this.init():console.warn("Help: Main container not found.")}init(){let{el:e}=this.DOM;e&&(this.DOM.funcs=Array.from(e.querySelectorAll(s.SELECTORS.func)),this.DOM.addressInput=e.querySelector(s.SELECTORS.addressInput),this.DOM.cmdModeSelect=e.querySelector(s.SELECTORS.cmdModeSelect),this.funcList=this.DOM.funcs.map(t=>new o(t)),this.restoreAddress(),this.bindEvents())}restoreAddress(){let{addressInput:e}=this.DOM;if(e){let t=localStorage.getItem("helpAddressInput");t&&(e.value=t,this.funcList.forEach(a=>a.updateAddr(t)))}}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM,a=d(n=>{let r=n.value;localStorage.setItem("helpAddressInput",r),this.funcList.forEach(i=>i.updateAddr(r))},50);e?.addEventListener("input",()=>a(e)),t?.addEventListener("change",n=>{let r=n.target;this.funcList.forEach(i=>i.updateMode(r.value))})}},o=class s{DOM;funcName;static SELECTORS={address:"[data-role='help-code-address']",args:"[data-role='help-code-args']",mode:"[data-code-mode]",paramInput:"[data-role='help-param-input']"};constructor(e){this.DOM={el:e,addrs:Array.from(e.querySelectorAll(s.SELECTORS.address)),args:Array.from(e.querySelectorAll(s.SELECTORS.args)),modes:Array.from(e.querySelectorAll(s.SELECTORS.mode)),paramInputs:Array.from(e.querySelectorAll(s.SELECTORS.paramInput))},this.funcName=e.dataset.func||null,this.initializeArgs(),this.bindEvents()}static sanitizeArgsInput(e){let t=e.dataset.param||"",a=e.value.trim();return t||console.warn("sanitizeArgsInput: param is missing in arg input dataset."),{paramName:t,paramValue:a}}bindEvents(){let e=d((t,a)=>{t&&this.updateArg(t,a)},50);this.DOM.el.addEventListener("input",t=>{let a=t.target;if(a.dataset.role==="help-param-input"){let{paramName:n,paramValue:r}=s.sanitizeArgsInput(a);e(n,r)}})}initializeArgs(){this.DOM.paramInputs.forEach(e=>{let{paramName:t,paramValue:a}=s.sanitizeArgsInput(e);t&&this.updateArg(t,a)})}updateArg(e,t){let a=u(t);this.DOM.args.filter(n=>n.dataset.arg===e).forEach(n=>{n.textContent=a||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let a=t.dataset.codeMode===e;t.classList.toggle("inline",a),t.classList.toggle("hidden",!a),t.dataset.copyContent=a?`help-cmd-${this.funcName}`:""})}},m=()=>new l;export{m as default}; diff --git a/gno.land/pkg/gnoweb/public/js/utils.js b/gno.land/pkg/gnoweb/public/js/utils.js index e27fb93bc1c..ce96def444a 100644 --- a/gno.land/pkg/gnoweb/public/js/utils.js +++ b/gno.land/pkg/gnoweb/public/js/utils.js @@ -1 +1 @@ -function r(t,n=250){let e;return function(...i){e!==void 0&&clearTimeout(e),e=setTimeout(()=>{t.apply(this,i)},n)}}export{r as debounce}; +function i(e,n=250){let t;return function(...r){t!==void 0&&clearTimeout(t),t=setTimeout(()=>{e.apply(this,r)},n)}}function a(e){return e.replace(/([$`"\\!|&;<>*?{}()])/g,"\\$1")}export{i as debounce,a as escapeShellSpecialChars}; From 7e1f5b5ade984f5a9b4083d9cffb5a0000ea7f89 Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 6 Feb 2025 14:59:51 +0100 Subject: [PATCH 30/60] fix(cmd/gno): only perform preprocessing in lint (#3597) fixes #3547 cc @leohhhn --- gnovm/cmd/gno/run.go | 2 +- gnovm/cmd/gno/tool_lint.go | 20 ++++++------ gnovm/cmd/gno/tool_lint_test.go | 42 ++++++++++++++++-------- gnovm/pkg/gnolang/debugger_test.go | 2 +- gnovm/pkg/gnolang/files_test.go | 6 ++-- gnovm/pkg/gnolang/machine.go | 45 ++++++++++++++++++++++++++ gnovm/pkg/repl/repl.go | 2 +- gnovm/pkg/test/imports.go | 52 ++++++++++++++++++++++++++---- gnovm/pkg/test/test.go | 5 +-- gnovm/tests/integ/init/gno.mod | 1 + gnovm/tests/integ/init/main.gno | 10 ++++++ 11 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 gnovm/tests/integ/init/gno.mod create mode 100644 gnovm/tests/integ/init/main.gno diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index 489016aa3d4..34bf818e8f5 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -93,7 +93,7 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { // init store and machine _, testStore := test.Store( - cfg.rootDir, false, + cfg.rootDir, stdin, stdout, stderr) if cfg.verbose { testStore.SetLogStoreOps(true) diff --git a/gnovm/cmd/gno/tool_lint.go b/gnovm/cmd/gno/tool_lint.go index ce3465b484e..6983175cea0 100644 --- a/gnovm/cmd/gno/tool_lint.go +++ b/gnovm/cmd/gno/tool_lint.go @@ -97,9 +97,9 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { hasError := false - bs, ts := test.Store( - rootDir, false, - nopReader{}, goio.Discard, goio.Discard, + bs, ts := test.StoreWithOptions( + rootDir, nopReader{}, goio.Discard, goio.Discard, + test.StoreOptions{PreprocessOnly: true}, ) for _, pkgPath := range pkgPaths { @@ -162,13 +162,10 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { tm := test.Machine(gs, goio.Discard, memPkg.Path) defer tm.Release() - // Check package - tm.RunMemPackage(memPkg, true) - // Check test files - testFiles := lintTestFiles(memPkg) + packageFiles := sourceAndTestFileset(memPkg) - tm.RunFiles(testFiles.Files...) + tm.PreprocessFiles(memPkg.Name, memPkg.Path, packageFiles, false, false) }) if hasRuntimeErr { hasError = true @@ -221,20 +218,21 @@ func lintTypeCheck(io commands.IO, memPkg *gnovm.MemPackage, testStore gno.Store return true, nil } -func lintTestFiles(memPkg *gnovm.MemPackage) *gno.FileSet { +func sourceAndTestFileset(memPkg *gnovm.MemPackage) *gno.FileSet { testfiles := &gno.FileSet{} for _, mfile := range memPkg.Files { if !strings.HasSuffix(mfile.Name, ".gno") { continue // Skip non-GNO files } - n, _ := gno.ParseFile(mfile.Name, mfile.Body) + n := gno.MustParseFile(mfile.Name, mfile.Body) if n == nil { continue // Skip empty files } // XXX: package ending with `_test` is not supported yet - if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") { + if !strings.HasSuffix(mfile.Name, "_filetest.gno") && + !strings.HasSuffix(string(n.PkgName), "_test") { // Keep only test files testfiles.AddFiles(n) } diff --git a/gnovm/cmd/gno/tool_lint_test.go b/gnovm/cmd/gno/tool_lint_test.go index 85b625fa367..3f9e5cd59ba 100644 --- a/gnovm/cmd/gno/tool_lint_test.go +++ b/gnovm/cmd/gno/tool_lint_test.go @@ -10,49 +10,63 @@ func TestLintApp(t *testing.T) { { args: []string{"tool", "lint"}, errShouldBe: "flag: help requested", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/run_main/"}, stderrShouldContain: "./../../tests/integ/run_main: gno.mod file not found in current or any parent directory (code=1)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, stderrShouldContain: "undefined_variables_test.gno:6:28: name toto not declared (code=2)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/package_not_declared/main.gno"}, stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/several-lint-errors/main.gno"}, - stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2)\n../../tests/integ/several-lint-errors/main.gno:6", + stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=3)\n../../tests/integ/several-lint-errors/main.gno:6", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, stderrShouldContain: func() string { lines := []string{ - "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", - "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)", - "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)", - "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)", + "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=3)", + "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=3)", + "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=3)", + "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=3)", } return strings.Join(lines, "\n") + "\n" }(), errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/minimalist_gnomod/"}, // TODO: raise an error because there is a gno.mod, but no .gno files - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/invalid_module_name/"}, // TODO: raise an error because gno.mod is invalid - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/invalid_gno_file/"}, stderrShouldContain: "../../tests/integ/invalid_gno_file/invalid.gno:1:1: expected 'package', found packag (code=2)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/typecheck_missing_return/"}, stderrShouldContain: "../../tests/integ/typecheck_missing_return/main.gno:5:1: missing return (code=4)", errShouldBe: "exit code: 1", }, + { + args: []string{"tool", "lint", "../../tests/integ/init/"}, + // stderr / stdout should be empty; the init function and statements + // should not be executed + }, // TODO: 'gno mod' is valid? // TODO: are dependencies valid? diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 926ff0595e6..a9e0a4834d5 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -39,7 +39,7 @@ func evalTest(debugAddr, in, file string) (out, err string) { err = strings.TrimSpace(strings.ReplaceAll(err, "../../tests/files/", "files/")) }() - _, testStore := test.Store(gnoenv.RootDir(), false, stdin, stdout, stderr) + _, testStore := test.Store(gnoenv.RootDir(), stdin, stdout, stderr) f := gnolang.MustReadFile(file) diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go index 2c82f6d8f29..31f04087855 100644 --- a/gnovm/pkg/gnolang/files_test.go +++ b/gnovm/pkg/gnolang/files_test.go @@ -45,9 +45,9 @@ func TestFiles(t *testing.T) { Error: io.Discard, Sync: *withSync, } - o.BaseStore, o.TestStore = test.Store( - rootDir, true, - nopReader{}, o.WriterForStore(), io.Discard, + o.BaseStore, o.TestStore = test.StoreWithOptions( + rootDir, nopReader{}, o.WriterForStore(), io.Discard, + test.StoreOptions{WithExtern: true}, ) return o } diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 75d12ac5402..f7d2cf10f2c 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -454,6 +454,51 @@ func (m *Machine) RunFiles(fns ...*FileNode) { m.runInitFromUpdates(pv, updates) } +// PreprocessFiles runs Preprocess on the given files. It is used to detect +// compile-time errors in the package. +func (m *Machine) PreprocessFiles(pkgName, pkgPath string, fset *FileSet, save, withOverrides bool) (*PackageNode, *PackageValue) { + if !withOverrides { + if err := checkDuplicates(fset); err != nil { + panic(fmt.Errorf("running package %q: %w", pkgName, err)) + } + } + pn := NewPackageNode(Name(pkgName), pkgPath, fset) + pv := pn.NewPackage() + pb := pv.GetBlock(m.Store) + m.SetActivePackage(pv) + m.Store.SetBlockNode(pn) + PredefineFileSet(m.Store, pn, fset) + for _, fn := range fset.Files { + fn = Preprocess(m.Store, pn, fn).(*FileNode) + // After preprocessing, save blocknodes to store. + SaveBlockNodes(m.Store, fn) + // Make block for fn. + // Each file for each *PackageValue gets its own file *Block, + // with values copied over from each file's + // *FileNode.StaticBlock. + fb := m.Alloc.NewBlock(fn, pb) + fb.Values = make([]TypedValue, len(fn.StaticBlock.Values)) + copy(fb.Values, fn.StaticBlock.Values) + pv.AddFileBlock(fn.Name, fb) + } + // Get new values across all files in package. + pn.PrepareNewValues(pv) + // save package value. + var throwaway *Realm + if save { + // store new package values and types + throwaway = m.saveNewPackageValuesAndTypes() + if throwaway != nil { + m.Realm = throwaway + } + m.resavePackageValues(throwaway) + if throwaway != nil { + m.Realm = nil + } + } + return pn, pv +} + // Add files to the package's *FileSet and run decls in them. // This will also run each init function encountered. // Returns the updated typed values of package. diff --git a/gnovm/pkg/repl/repl.go b/gnovm/pkg/repl/repl.go index b0944d21646..fff80d672dc 100644 --- a/gnovm/pkg/repl/repl.go +++ b/gnovm/pkg/repl/repl.go @@ -125,7 +125,7 @@ func NewRepl(opts ...ReplOption) *Repl { r.stderr = &b r.storeFunc = func() gno.Store { - _, st := test.Store(gnoenv.RootDir(), false, r.stdin, r.stdout, r.stderr) + _, st := test.Store(gnoenv.RootDir(), r.stdin, r.stdout, r.stderr) return st } diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index a8dd709e501..95302ecffb0 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -25,22 +25,56 @@ import ( storetypes "github.com/gnolang/gno/tm2/pkg/store/types" ) +type StoreOptions struct { + // WithExtern interprets imports of packages under "github.com/gnolang/gno/_test/" + // as imports under the directory in gnovm/tests/files/extern. + // This should only be used for GnoVM internal filetests (gnovm/tests/files). + WithExtern bool + + // PreprocessOnly instructs the PackageGetter to run the imported files using + // [gno.Machine.PreprocessFiles]. It avoids executing code for contexts + // which only intend to perform a type check, ie. `gno lint`. + PreprocessOnly bool +} + // NOTE: this isn't safe, should only be used for testing. func Store( rootDir string, - withExtern bool, stdin io.Reader, stdout, stderr io.Writer, ) ( baseStore storetypes.CommitStore, resStore gno.Store, ) { + return StoreWithOptions(rootDir, stdin, stdout, stderr, StoreOptions{}) +} + +// StoreWithOptions is a variant of [Store] which additionally accepts a +// [StoreOptions] argument. +func StoreWithOptions( + rootDir string, + stdin io.Reader, + stdout, stderr io.Writer, + opts StoreOptions, +) ( + baseStore storetypes.CommitStore, + resStore gno.Store, +) { + processMemPackage := func(m *gno.Machine, memPkg *gnovm.MemPackage, save bool) (*gno.PackageNode, *gno.PackageValue) { + return m.RunMemPackage(memPkg, save) + } + if opts.PreprocessOnly { + processMemPackage = func(m *gno.Machine, memPkg *gnovm.MemPackage, save bool) (*gno.PackageNode, *gno.PackageValue) { + m.Store.AddMemPackage(memPkg) + return m.PreprocessFiles(memPkg.Name, memPkg.Path, gno.ParseMemPackage(memPkg), save, false) + } + } getPackage := func(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) { if pkgPath == "" { panic(fmt.Sprintf("invalid zero package path in testStore().pkgGetter")) } - if withExtern { + if opts.WithExtern { // if _test package... const testPath = "github.com/gnolang/gno/_test/" if strings.HasPrefix(pkgPath, testPath) { @@ -54,7 +88,7 @@ func Store( Store: store, Context: ctx, }) - return m2.RunMemPackage(memPkg, true) + return processMemPackage(m2, memPkg, true) } } @@ -129,7 +163,7 @@ func Store( } // Load normal stdlib. - pn, pv = loadStdlib(rootDir, pkgPath, store, stdout) + pn, pv = loadStdlib(rootDir, pkgPath, store, stdout, opts.PreprocessOnly) if pn != nil { return } @@ -150,8 +184,7 @@ func Store( Store: store, Context: ctx, }) - pn, pv = m2.RunMemPackage(memPkg, true) - return + return processMemPackage(m2, memPkg, true) } return nil, nil } @@ -164,7 +197,7 @@ func Store( return } -func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gno.PackageNode, *gno.PackageValue) { +func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer, preprocessOnly bool) (*gno.PackageNode, *gno.PackageValue) { dirs := [...]string{ // Normal stdlib path. filepath.Join(rootDir, "gnovm", "stdlibs", pkgPath), @@ -202,6 +235,11 @@ func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gn Output: stdout, Store: store, }) + if preprocessOnly { + m2.Store.AddMemPackage(memPkg) + return m2.PreprocessFiles(memPkg.Name, memPkg.Path, gno.ParseMemPackage(memPkg), true, true) + } + // TODO: make this work when using gno lint. return m2.RunMemPackageWithOverrides(memPkg, true) } diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index d06540761d7..92a867e1886 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -139,10 +139,7 @@ func NewTestOptions(rootDir string, stdin io.Reader, stdout, stderr io.Writer) * Output: stdout, Error: stderr, } - opts.BaseStore, opts.TestStore = Store( - rootDir, false, - stdin, opts.WriterForStore(), stderr, - ) + opts.BaseStore, opts.TestStore = Store(rootDir, stdin, opts.WriterForStore(), stderr) return opts } diff --git a/gnovm/tests/integ/init/gno.mod b/gnovm/tests/integ/init/gno.mod new file mode 100644 index 00000000000..28c7e51b750 --- /dev/null +++ b/gnovm/tests/integ/init/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/init diff --git a/gnovm/tests/integ/init/main.gno b/gnovm/tests/integ/init/main.gno new file mode 100644 index 00000000000..88cfafb9f24 --- /dev/null +++ b/gnovm/tests/integ/init/main.gno @@ -0,0 +1,10 @@ +package main + +var _ = func() int { + println("HELLO HELLO!!") + return 1 +}() + +func init() { + println("HELLO WORLD!") +} From dd0360dc3c209585e6a7f5fac2f8f9341bdaf46d Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:08:11 +0100 Subject: [PATCH 31/60] chore: remove govdao dependency in `r/gov/dao/bridge` (#3523) ## Description Cherry-picked from: #3166 This PR removes the `r/gov/dao/v2` import from the `r/gov/dao/bridge` realm. Previously, cyclic imports were easily created if a realm imported the bridge realm (to expose a executor constructor func), and GovDAO imported that realm. In my case: - `r/sys/users` imported `r/gov/dao/bridge` to create executor constructors - `r/gov/dao/bridge` imported `r/gov/dao/v2` to have access to the implementation - `r/gov/dao/v2` imported `r/sys/users` to have access to user data. - -> creating a cyclic dependency which is not allowed. This is fixed by modifying the `r/gov/dao/v2` contract to expose a safe-object, which in turn exposes all top-level functions as methods. Then, the bridge uses a one-time init package which will load v2 into the bridge in genesis, as per @moul's comment. --------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/r/gov/dao/bridge/bridge.gno | 46 +++++++++++++++---- .../gno.land/r/gov/dao/bridge/bridge_test.gno | 45 ++++++++++++++++-- examples/gno.land/r/gov/dao/bridge/v2.gno | 42 ----------------- examples/gno.land/r/gov/dao/init/gno.mod | 1 + examples/gno.land/r/gov/dao/init/init.gno | 13 ++++++ examples/gno.land/r/gov/dao/v2/dao.gno | 19 ++++++-- examples/gno.land/r/gov/dao/v2/poc.gno | 10 ++-- .../gno.land/r/gov/dao/v2/prop1_filetest.gno | 8 ++-- .../gno.land/r/gov/dao/v2/prop2_filetest.gno | 7 +-- .../gno.land/r/gov/dao/v2/prop3_filetest.gno | 11 +++-- .../gno.land/r/gov/dao/v2/prop4_filetest.gno | 1 + .../gno.land/r/sys/params/params_test.gno | 6 ++- 12 files changed, 130 insertions(+), 79 deletions(-) delete mode 100644 examples/gno.land/r/gov/dao/bridge/v2.gno create mode 100644 examples/gno.land/r/gov/dao/init/gno.mod create mode 100644 examples/gno.land/r/gov/dao/init/init.gno diff --git a/examples/gno.land/r/gov/dao/bridge/bridge.gno b/examples/gno.land/r/gov/dao/bridge/bridge.gno index ba47978f33f..87a12d94e54 100644 --- a/examples/gno.land/r/gov/dao/bridge/bridge.gno +++ b/examples/gno.land/r/gov/dao/bridge/bridge.gno @@ -3,34 +3,60 @@ package bridge import ( "std" + "gno.land/p/demo/dao" "gno.land/p/demo/ownable" ) -const initialOwner = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @moul +const ( + initialOwner = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @moul + loader = "gno.land/r/gov/dao/init" +) -var b *Bridge +var ( + b *Bridge + Ownable = ownable.NewWithAddress(initialOwner) +) // Bridge is the active GovDAO // implementation bridge type Bridge struct { - *ownable.Ownable - dao DAO } // init constructs the initial GovDAO implementation func init() { b = &Bridge{ - Ownable: ownable.NewWithAddress(initialOwner), - dao: &govdaoV2{}, + dao: nil, // initially set via r/gov/dao/init + } +} + +// LoadGovDAO loads the initial version of GovDAO into the bridge +// All changes to b.dao need to be done via GovDAO proposals after +func LoadGovDAO(d DAO) { + if std.PrevRealm().PkgPath() != loader { + panic("unauthorized") } + + b.dao = d } -// SetDAO sets the currently active GovDAO implementation -func SetDAO(dao DAO) { - b.AssertCallerIsOwner() +// NewGovDAOImplChangeExecutor allows creating a GovDAO proposal +// Which will upgrade the GovDAO version inside the bridge +func NewGovDAOImplChangeExecutor(newImpl DAO) dao.Executor { + callback := func() error { + b.dao = newImpl + return nil + } + + return b.dao.NewGovDAOExecutor(callback) +} - b.dao = dao +// SetGovDAO allows the admin to set the GovDAO version manually +// This functionality can be fully disabled by Ownable.DropOwnership(), +// making this realm fully managed by GovDAO. +func SetGovDAO(d DAO) { + Ownable.AssertCallerIsOwner() + b.dao = d } // GovDAO returns the current GovDAO implementation diff --git a/examples/gno.land/r/gov/dao/bridge/bridge_test.gno b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno index 38b5d4be257..da06db293ab 100644 --- a/examples/gno.land/r/gov/dao/bridge/bridge_test.gno +++ b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno @@ -1,9 +1,8 @@ package bridge import ( - "testing" - "std" + "testing" "gno.land/p/demo/dao" "gno.land/p/demo/ownable" @@ -27,11 +26,47 @@ func TestBridge_DAO(t *testing.T) { uassert.Equal(t, proposalID, GovDAO().Propose(dao.ProposalRequest{})) } +func TestBridge_LoadGovDAO(t *testing.T) { + t.Run("invalid initializer path", func(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/init")) // invalid loader + + // Attempt to set a new DAO implementation + uassert.PanicsWithMessage(t, "unauthorized", func() { + LoadGovDAO(&mockDAO{}) + }) + }) + + t.Run("valid loader", func(t *testing.T) { + var ( + initializer = "gno.land/r/gov/dao/init" + proposalID = uint64(10) + mockDAO = &mockDAO{ + proposeFn: func(_ dao.ProposalRequest) uint64 { + return proposalID + }, + } + ) + + std.TestSetRealm(std.NewCodeRealm(initializer)) + + // Attempt to set a new DAO implementation + uassert.NotPanics(t, func() { + LoadGovDAO(mockDAO) + }) + + uassert.Equal( + t, + mockDAO.Propose(dao.ProposalRequest{}), + GovDAO().Propose(dao.ProposalRequest{}), + ) + }) +} + func TestBridge_SetDAO(t *testing.T) { t.Run("invalid owner", func(t *testing.T) { // Attempt to set a new DAO implementation uassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() { - SetDAO(&mockDAO{}) + SetGovDAO(&mockDAO{}) }) }) @@ -49,10 +84,10 @@ func TestBridge_SetDAO(t *testing.T) { std.TestSetOrigCaller(addr) - b.Ownable = ownable.NewWithAddress(addr) + Ownable = ownable.NewWithAddress(addr) urequire.NotPanics(t, func() { - SetDAO(mockDAO) + SetGovDAO(mockDAO) }) uassert.Equal( diff --git a/examples/gno.land/r/gov/dao/bridge/v2.gno b/examples/gno.land/r/gov/dao/bridge/v2.gno deleted file mode 100644 index 216419cf31d..00000000000 --- a/examples/gno.land/r/gov/dao/bridge/v2.gno +++ /dev/null @@ -1,42 +0,0 @@ -package bridge - -import ( - "gno.land/p/demo/dao" - "gno.land/p/demo/membstore" - govdao "gno.land/r/gov/dao/v2" -) - -// govdaoV2 is a wrapper for interacting with the /r/gov/dao/v2 Realm -type govdaoV2 struct{} - -func (g *govdaoV2) Propose(request dao.ProposalRequest) uint64 { - return govdao.Propose(request) -} - -func (g *govdaoV2) VoteOnProposal(id uint64, option dao.VoteOption) { - govdao.VoteOnProposal(id, option) -} - -func (g *govdaoV2) ExecuteProposal(id uint64) { - govdao.ExecuteProposal(id) -} - -func (g *govdaoV2) GetPropStore() dao.PropStore { - return govdao.GetPropStore() -} - -func (g *govdaoV2) GetMembStore() membstore.MemberStore { - return govdao.GetMembStore() -} - -func (g *govdaoV2) NewGovDAOExecutor(cb func() error) dao.Executor { - return govdao.NewGovDAOExecutor(cb) -} - -func (g *govdaoV2) NewMemberPropExecutor(cb func() []membstore.Member) dao.Executor { - return govdao.NewMemberPropExecutor(cb) -} - -func (g *govdaoV2) NewMembStoreImplExecutor(cb func() membstore.MemberStore) dao.Executor { - return govdao.NewMembStoreImplExecutor(cb) -} diff --git a/examples/gno.land/r/gov/dao/init/gno.mod b/examples/gno.land/r/gov/dao/init/gno.mod new file mode 100644 index 00000000000..40541f4f152 --- /dev/null +++ b/examples/gno.land/r/gov/dao/init/gno.mod @@ -0,0 +1 @@ +module gno.land/r/gov/dao/init diff --git a/examples/gno.land/r/gov/dao/init/init.gno b/examples/gno.land/r/gov/dao/init/init.gno new file mode 100644 index 00000000000..39bdbedba83 --- /dev/null +++ b/examples/gno.land/r/gov/dao/init/init.gno @@ -0,0 +1,13 @@ +// Package init's only task is to load the initial GovDAO version into the bridge. +// This is done to avoid gov/dao/v2 as a bridge dependency, +// As this can often lead to cyclic dependency errors. +package init + +import ( + "gno.land/r/gov/dao/bridge" + govdao "gno.land/r/gov/dao/v2" +) + +func init() { + bridge.LoadGovDAO(govdao.GovDAO) +} diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 5ee8e63236a..d69f9901301 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -11,8 +11,17 @@ import ( var ( d *simpledao.SimpleDAO // the current active DAO implementation members membstore.MemberStore // the member store + + // GovDAO exposes all functions of this contract as methods + GovDAO = &DAO{} ) +// DAO is an empty struct that allows all +// functions of this realm to be methods instead of functions +// This allows a registry, such as r/gov/dao/bridge +// to take this object and match it to a required interface +type DAO struct{} + const daoPkgPath = "gno.land/r/gov/dao/v2" func init() { @@ -33,7 +42,7 @@ func init() { // Propose is designed to be called by another contract or with // `maketx run`, not by a `maketx call`. -func Propose(request dao.ProposalRequest) uint64 { +func (_ DAO) Propose(request dao.ProposalRequest) uint64 { idx, err := d.Propose(request) if err != nil { panic(err) @@ -43,25 +52,25 @@ func Propose(request dao.ProposalRequest) uint64 { } // VoteOnProposal casts a vote for the given proposal -func VoteOnProposal(id uint64, option dao.VoteOption) { +func (_ DAO) VoteOnProposal(id uint64, option dao.VoteOption) { if err := d.VoteOnProposal(id, option); err != nil { panic(err) } } // ExecuteProposal executes the proposal -func ExecuteProposal(id uint64) { +func (_ DAO) ExecuteProposal(id uint64) { if err := d.ExecuteProposal(id); err != nil { panic(err) } } // GetPropStore returns the active proposal store -func GetPropStore() dao.PropStore { +func (_ DAO) GetPropStore() dao.PropStore { return d } // GetMembStore returns the active member store -func GetMembStore() membstore.MemberStore { +func (_ DAO) GetMembStore() membstore.MemberStore { return members } diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index 30d8a403f6e..81bdc7c9b12 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -13,7 +13,7 @@ import ( var errNoChangesProposed = errors.New("no set changes proposed") // NewGovDAOExecutor creates the govdao wrapped callback executor -func NewGovDAOExecutor(cb func() error) dao.Executor { +func (_ DAO) NewGovDAOExecutor(cb func() error) dao.Executor { if cb == nil { panic(errNoChangesProposed) } @@ -25,7 +25,7 @@ func NewGovDAOExecutor(cb func() error) dao.Executor { } // NewMemberPropExecutor returns the GOVDAO member change executor -func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { +func (_ DAO) NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { if changesFn == nil { panic(errNoChangesProposed) } @@ -65,10 +65,10 @@ func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { return errs } - return NewGovDAOExecutor(callback) + return GovDAO.NewGovDAOExecutor(callback) } -func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor { +func (_ DAO) NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor { if changeFn == nil { panic(errNoChangesProposed) } @@ -79,7 +79,7 @@ func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executo return nil } - return NewGovDAOExecutor(callback) + return GovDAO.NewGovDAOExecutor(callback) } // setMembStoreImpl sets a new dao.MembStore implementation diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index 7d8975e1fe8..c8ea983cc73 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -12,11 +12,13 @@ import ( "gno.land/p/demo/dao" pVals "gno.land/p/sys/validators" + _ "gno.land/r/gov/dao/init" // so that the govdao initializer is executed govdao "gno.land/r/gov/dao/v2" validators "gno.land/r/sys/validators/v2" ) func init() { + changesFn := func() []pVals.Validator { return []pVals.Validator{ { @@ -51,7 +53,7 @@ func init() { Executor: executor, } - govdao.Propose(prop) + govdao.GovDAO.Propose(prop) } func main() { @@ -60,13 +62,13 @@ func main() { println("--") println(govdao.Render("0")) println("--") - govdao.VoteOnProposal(0, dao.YesVote) + govdao.GovDAO.VoteOnProposal(0, dao.YesVote) println("--") println(govdao.Render("0")) println("--") println(validators.Render("")) println("--") - govdao.ExecuteProposal(0) + govdao.GovDAO.ExecuteProposal(0) println("--") println(govdao.Render("0")) println("--") diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index 84a64bc4ee2..f85373a471c 100644 --- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -5,6 +5,7 @@ import ( "gno.land/p/demo/dao" gnoblog "gno.land/r/gnoland/blog" + _ "gno.land/r/gov/dao/init" // so that the govdao initializer is executed govdao "gno.land/r/gov/dao/v2" ) @@ -28,7 +29,7 @@ func init() { Executor: ex, } - govdao.Propose(prop) + govdao.GovDAO.Propose(prop) } func main() { @@ -37,13 +38,13 @@ func main() { println("--") println(govdao.Render("0")) println("--") - govdao.VoteOnProposal(0, "YES") + govdao.GovDAO.VoteOnProposal(0, "YES") println("--") println(govdao.Render("0")) println("--") println(gnoblog.Render("")) println("--") - govdao.ExecuteProposal(0) + govdao.GovDAO.ExecuteProposal(0) println("--") println(govdao.Render("0")) println("--") diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno index 068f520e7e2..4032ba41d55 100644 --- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -6,6 +6,7 @@ import ( "gno.land/p/demo/dao" "gno.land/p/demo/membstore" "gno.land/r/gov/dao/bridge" + _ "gno.land/r/gov/dao/init" // so that the govdao initializer is executed govdao "gno.land/r/gov/dao/v2" ) @@ -34,7 +35,7 @@ func init() { prop := dao.ProposalRequest{ Title: title, Description: description, - Executor: govdao.NewMemberPropExecutor(memberFn), + Executor: govdao.GovDAO.NewMemberPropExecutor(memberFn), } bridge.GovDAO().Propose(prop) @@ -42,25 +43,25 @@ func init() { func main() { println("--") - println(govdao.GetMembStore().Size()) + println(govdao.GovDAO.GetMembStore().Size()) println("--") println(govdao.Render("")) println("--") println(govdao.Render("0")) println("--") - govdao.VoteOnProposal(0, "YES") + govdao.GovDAO.VoteOnProposal(0, "YES") println("--") println(govdao.Render("0")) println("--") println(govdao.Render("")) println("--") - govdao.ExecuteProposal(0) + govdao.GovDAO.ExecuteProposal(0) println("--") println(govdao.Render("0")) println("--") println(govdao.Render("")) println("--") - println(govdao.GetMembStore().Size()) + println(govdao.GovDAO.GetMembStore().Size()) } // Output: diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno index 13ca572c512..49326495dac 100644 --- a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno @@ -3,6 +3,7 @@ package main import ( "gno.land/p/demo/dao" "gno.land/r/gov/dao/bridge" + _ "gno.land/r/gov/dao/init" // so that the govdao initializer is executed govdaov2 "gno.land/r/gov/dao/v2" "gno.land/r/sys/params" ) diff --git a/examples/gno.land/r/sys/params/params_test.gno b/examples/gno.land/r/sys/params/params_test.gno index eaa1ad039d3..a15da1e7499 100644 --- a/examples/gno.land/r/sys/params/params_test.gno +++ b/examples/gno.land/r/sys/params/params_test.gno @@ -1,6 +1,10 @@ package params -import "testing" +import ( + "testing" + + _ "gno.land/r/gov/dao/init" // so that loader.init is executed +) // Testing this package is limited because it only contains an `std.Set` method // without a corresponding `std.Get` method. For comprehensive testing, refer to From 37ab807315fa751531e69d28d533524b49ffb8b6 Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 6 Feb 2025 15:40:06 +0100 Subject: [PATCH 32/60] test(cmd/gnoland): add test to ensure to not import tests/stdlibs (#3589) Fixes #3585 --- gno.land/cmd/gnoland/imports_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 gno.land/cmd/gnoland/imports_test.go diff --git a/gno.land/cmd/gnoland/imports_test.go b/gno.land/cmd/gnoland/imports_test.go new file mode 100644 index 00000000000..c5ae81599b4 --- /dev/null +++ b/gno.land/cmd/gnoland/imports_test.go @@ -0,0 +1,20 @@ +package main + +import ( + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNoTestingStdlibImport(t *testing.T) { + // See: https://github.com/gnolang/gno/issues/3585 + // The gno.land binary should not import testing stdlibs, which contain unsafe + // code in the respective native bindings. + + res, err := exec.Command("go", "list", "-f", `{{ join .Deps "\n" }}`, ".").CombinedOutput() + require.NoError(t, err) + assert.Contains(t, string(res), "github.com/gnolang/gno/gnovm/stdlibs\n", "should contain normal stdlibs") + assert.NotContains(t, string(res), "github.com/gnolang/gno/gnovm/tests/stdlibs\n", "should not contain test stdlibs") +} From a01a030af45dcba60f16f571d1f7232e139b9bf5 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:15:36 +0100 Subject: [PATCH 33/60] chore: make codecov only care about newlines (#3277) ![CleanShot 2024-12-05 at 17 01 41](https://github.com/user-attachments/assets/454f769b-9b87-418a-9499-4cf28ab763ad) RFC Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> Co-authored-by: Morgan --- .github/codecov.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index f0cb9583cf2..d973bcce859 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -10,19 +10,10 @@ coverage: round: down precision: 2 status: - project: + patch: # new lines default: - target: auto - threshold: 5 # Let's decrease this later. - base: parent - if_no_uploads: error - if_not_found: success - if_ci_failed: error - only_pulls: false - patch: - default: - target: auto - threshold: 5 # Let's decrease this later. + target: 80 + threshold: 10 base: auto if_no_uploads: error if_not_found: success From 08d29a50d7ff9762417da205e1bab48b70c081ab Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Fri, 7 Feb 2025 01:04:03 +0900 Subject: [PATCH 34/60] feat(gnoweb): display network info (#3679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces a Network Info section in the gnoweb interface, displaying details such as `RPC` and `ChainID`. - It adds a new icon in the search bar, which triggers a modal (purely CSS-based, no JavaScript) to display these network details. - The modal is fully interactive, allowing users to close it by clicking either on the close button or anywhere outside the modal. This enhancement improves visibility and accessibility of network info from gnoweb without relying on JavaScript. Capture d’écran 2025-02-05 à 18 32 41 Capture d’écran 2025-02-05 à 18 32 50 --------- Co-authored-by: Morgan --- .../pkg/gnoweb/components/layout_header.go | 2 + .../pkg/gnoweb/components/layouts/header.html | 69 ++++++++++++++++--- gno.land/pkg/gnoweb/components/ui/icons.html | 45 ++++++++++++ gno.land/pkg/gnoweb/frontend/css/tx.config.js | 1 + gno.land/pkg/gnoweb/handler.go | 2 + gno.land/pkg/gnoweb/public/styles.css | 2 +- 6 files changed, 111 insertions(+), 10 deletions(-) diff --git a/gno.land/pkg/gnoweb/components/layout_header.go b/gno.land/pkg/gnoweb/components/layout_header.go index b85efde5f85..d446212c6e0 100644 --- a/gno.land/pkg/gnoweb/components/layout_header.go +++ b/gno.land/pkg/gnoweb/components/layout_header.go @@ -16,6 +16,8 @@ type HeaderData struct { Breadcrumb BreadcrumbData WebQuery url.Values Links []HeaderLink + ChainId string + Remote string } func StaticHeaderLinks(realmPath string, webQuery url.Values) []HeaderLink { diff --git a/gno.land/pkg/gnoweb/components/layouts/header.html b/gno.land/pkg/gnoweb/components/layouts/header.html index 5743d0a82b6..851833b1dc0 100644 --- a/gno.land/pkg/gnoweb/components/layouts/header.html +++ b/gno.land/pkg/gnoweb/components/layouts/header.html @@ -6,16 +6,67 @@ Gno username profile pic -
{{ range .Links }} {{ template "ui/header_link" . }} {{ end }}
diff --git a/gno.land/pkg/gnoweb/components/ui/icons.html b/gno.land/pkg/gnoweb/components/ui/icons.html index feef8226be7..f1145d74359 100644 --- a/gno.land/pkg/gnoweb/components/ui/icons.html +++ b/gno.land/pkg/gnoweb/components/ui/icons.html @@ -120,5 +120,50 @@ fill="transparent" /> + + + + + + + + + + + + + + + {{ end }} diff --git a/gno.land/pkg/gnoweb/frontend/css/tx.config.js b/gno.land/pkg/gnoweb/frontend/css/tx.config.js index 451688d7da6..06aa685676a 100644 --- a/gno.land/pkg/gnoweb/frontend/css/tx.config.js +++ b/gno.land/pkg/gnoweb/frontend/css/tx.config.js @@ -26,6 +26,7 @@ export default { borderRadius: { sm: `${pxToRem(4)}rem`, DEFAULT: `${pxToRem(6)}rem`, + full: "9999px", }, colors: { light: "#FFFFFF", diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index ac39f4ce0f9..4c1dae31261 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -123,6 +123,8 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components RealmPath: gnourl.Encode(EncodePath | EncodeArgs | EncodeQuery | EncodeNoEscape), Breadcrumb: breadcrumb, WebQuery: gnourl.WebQuery, + ChainId: h.Static.ChainId, + Remote: h.Static.RemoteHelp, } switch { diff --git a/gno.land/pkg/gnoweb/public/styles.css b/gno.land/pkg/gnoweb/public/styles.css index ec575bb3735..96e768a313e 100644 --- a/gno.land/pkg/gnoweb/public/styles.css +++ b/gno.land/pkg/gnoweb/public/styles.css @@ -1,3 +1,3 @@ @font-face{font-family:Roboto;font-style:normal;font-weight:900;font-display:swap;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-family:Inter var;font-weight:100 900;font-display:block;font-style:oblique 0deg 10deg;src:url(fonts/intervar/Intervar.woff2) format("woff2")}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: } -/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter var,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji,sans-serif;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-view{overflow-wrap:break-word;padding-top:1.5rem;font-size:1rem}@media (min-width:51.25rem){.realm-view{padding-top:2.5rem}}.realm-view>:first-child{margin-top:0!important}.realm-view a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-view a:hover{text-decoration-line:underline}.realm-view h1,.realm-view h2,.realm-view h3,.realm-view h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-view h2,.realm-view h2 *{font-weight:700}.realm-view h3,.realm-view h3 *,.realm-view h4,.realm-view h4 *{font-weight:600}.realm-view h1+h2,.realm-view h2+h3,.realm-view h3+h4{margin-top:1rem}.realm-view h1{font-size:2.375rem;font-weight:700}.realm-view h2{font-size:1.5rem}.realm-view h3{margin-top:2.5rem;font-size:1.25rem}.realm-view h3,.realm-view h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-view p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-view strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-view strong *{font-weight:700}.realm-view em{font-style:oblique 14deg}.realm-view blockquote{margin-top:1rem;margin-bottom:1rem;border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 14deg}.realm-view ol,.realm-view ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-view ol li,.realm-view ul li{margin-bottom:.5rem}.realm-view img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-view figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-view figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-view :not(pre)>code,.realm-view pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-view pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-view hr{margin-top:2.5rem;margin-bottom:2.5rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-view table{margin-top:2rem;margin-bottom:2rem;display:block;width:100%;max-width:100%;border-collapse:collapse;overflow-x:auto}.realm-view td,.realm-view th{white-space:normal;overflow-wrap:break-word;border-width:1px;padding:.5rem 1rem}.realm-view th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-view caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 14deg;quotes:"“" "”" "‘" "’"}.realm-view q:after,.realm-view q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-view q:after{content:close-quote}.realm-view q:before{content:open-quote}.realm-view q:after{content:close-quote}.realm-view ol ol,.realm-view ol ul,.realm-view ul ol,.realm-view ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-view ul{list-style-type:disc}.realm-view ol{list-style-type:decimal}.realm-view abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-view details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-view summary{cursor:pointer;font-weight:700}.realm-view a code{color:inherit}.realm-view video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-view math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-view small{font-size:.875rem}.realm-view del{text-decoration-line:line-through}.realm-view sub{vertical-align:sub;font-size:.75rem}.realm-view sup{vertical-align:super;font-size:.75rem}.realm-view button,.realm-view input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-view{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-view>pre a:hover{text-decoration-line:none}main :is(.realm-view,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-view,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.dev-mode .toc-expend-btn{cursor:pointer;border-width:1px;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.dev-mode .toc-expend-btn:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}@media (min-width:51.25rem){.dev-mode .toc-expend-btn{border-style:none;background-color:transparent}}.dev-mode #sidebar-summary{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}@media (min-width:51.25rem){.dev-mode #sidebar-summary{background-color:transparent}}.dev-mode .toc-nav{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.z-1{z-index:1}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-dense{grid-auto-flow:dense}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-gray-100:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:mt-8:first-child{margin-top:2rem}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}.lg\:text-200{font-size:1rem}.lg\:font-semibold{font-weight:600}.lg\:first\:mt-0:first-child{margin-top:0}.lg\:hover\:bg-transparent:hover{background-color:transparent}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}} \ No newline at end of file +/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter var,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji,sans-serif;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-view{overflow-wrap:break-word;padding-top:1.5rem;font-size:1rem}@media (min-width:51.25rem){.realm-view{padding-top:2.5rem}}.realm-view>:first-child{margin-top:0!important}.realm-view a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-view a:hover{text-decoration-line:underline}.realm-view h1,.realm-view h2,.realm-view h3,.realm-view h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-view h2,.realm-view h2 *{font-weight:700}.realm-view h3,.realm-view h3 *,.realm-view h4,.realm-view h4 *{font-weight:600}.realm-view h1+h2,.realm-view h2+h3,.realm-view h3+h4{margin-top:1rem}.realm-view h1{font-size:2.375rem;font-weight:700}.realm-view h2{font-size:1.5rem}.realm-view h3{margin-top:2.5rem;font-size:1.25rem}.realm-view h3,.realm-view h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-view p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-view strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-view strong *{font-weight:700}.realm-view em{font-style:oblique 14deg}.realm-view blockquote{margin-top:1rem;margin-bottom:1rem;border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 14deg}.realm-view ol,.realm-view ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-view ol li,.realm-view ul li{margin-bottom:.5rem}.realm-view img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-view figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-view figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-view :not(pre)>code,.realm-view pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-view pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-view hr{margin-top:2.5rem;margin-bottom:2.5rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-view table{margin-top:2rem;margin-bottom:2rem;display:block;width:100%;max-width:100%;border-collapse:collapse;overflow-x:auto}.realm-view td,.realm-view th{white-space:normal;overflow-wrap:break-word;border-width:1px;padding:.5rem 1rem}.realm-view th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-view caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 14deg;quotes:"“" "”" "‘" "’"}.realm-view q:after,.realm-view q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-view q:after{content:close-quote}.realm-view q:before{content:open-quote}.realm-view q:after{content:close-quote}.realm-view ol ol,.realm-view ol ul,.realm-view ul ol,.realm-view ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-view ul{list-style-type:disc}.realm-view ol{list-style-type:decimal}.realm-view abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-view details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-view summary{cursor:pointer;font-weight:700}.realm-view a code{color:inherit}.realm-view video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-view math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-view small{font-size:.875rem}.realm-view del{text-decoration-line:line-through}.realm-view sub{vertical-align:sub;font-size:.75rem}.realm-view sup{vertical-align:super;font-size:.75rem}.realm-view button,.realm-view input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-view{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-view>pre a:hover{text-decoration-line:none}main :is(.realm-view,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-view,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.dev-mode .toc-expend-btn{cursor:pointer;border-width:1px;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.dev-mode .toc-expend-btn:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}@media (min-width:51.25rem){.dev-mode .toc-expend-btn{border-style:none;background-color:transparent}}.dev-mode #sidebar-summary{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}@media (min-width:51.25rem){.dev-mode #sidebar-summary{background-color:transparent}}.dev-mode .toc-nav{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-1{bottom:.25rem}.left-0{left:0}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-px{right:1px}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.top-px{top:1px}.z-1{z-index:1}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-\[calc\(100\%-2px\)\]{height:calc(100% - 2px)}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-72{width:18rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.-translate-x-full{--tw-translate-x:-100%}.-translate-x-full,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-dense{grid-auto-flow:dense}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.rounded-r{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-gray-100:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:mt-8:first-child{margin-top:2rem}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:visible{visibility:visible}.peer:checked~.peer-checked\:opacity-100{opacity:1}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:flex{display:flex}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}.md\:pr-8{padding-right:2rem}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}.lg\:text-200{font-size:1rem}.lg\:font-semibold{font-weight:600}.lg\:first\:mt-0:first-child{margin-top:0}.lg\:hover\:bg-transparent:hover{background-color:transparent}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}} \ No newline at end of file From 29c3ee60b1d4cacaf19c2693589dc0075bcb0b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Thu, 6 Feb 2025 17:27:21 +0100 Subject: [PATCH 35/60] chore(examples): change AVL pager to expect a read only tree (#3673) Using a read only tree now that there is an interface for it would make sense sense for the pager --- examples/gno.land/p/demo/avl/pager/pager.gno | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index f5f909a473d..6a77ba3eb6f 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -5,13 +5,13 @@ import ( "net/url" "strconv" - "gno.land/p/demo/avl" + "gno.land/p/demo/avl/rotree" "gno.land/p/demo/ufmt" ) // Pager is a struct that holds the AVL tree and pagination parameters. type Pager struct { - Tree avl.ITree + Tree rotree.IReadOnlyTree PageQueryParam string SizeQueryParam string DefaultPageSize int @@ -37,7 +37,7 @@ type Item struct { } // NewPager creates a new Pager with default values. -func NewPager(tree avl.ITree, defaultPageSize int, reversed bool) *Pager { +func NewPager(tree rotree.IReadOnlyTree, defaultPageSize int, reversed bool) *Pager { return &Pager{ Tree: tree, PageQueryParam: "page", From ce6a4aa5e8935b6dd8befc22f40d1f6a3bd628a9 Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Fri, 7 Feb 2025 01:20:51 +0800 Subject: [PATCH 36/60] fix(gnovm): correct filetest directive behavior (#3697) The test result is misleading. Without an output directive set, the current logic does not provide a prompt even when actual output occurs. ```go package main func main() { println("ok") } // Error: // panic xxx ``` ## before fix: === RUN TestFiles === PAUSE TestFiles === CONT TestFiles === RUN TestFiles/a111.gno --- PASS: TestFiles (0.14s) --- PASS: TestFiles/a111.gno (0.01s) PASS ok command-line-arguments 1.619s ## after fix: === RUN TestFiles === PAUSE TestFiles === CONT TestFiles === RUN TestFiles/a111.gno files_test.go:92: unexpected output: ok --- FAIL: TestFiles (0.13s) --- FAIL: TestFiles/a111.gno (0.01s) FAIL FAIL command-line-arguments 1.479s FAIL --------- Co-authored-by: Morgan --- gnovm/pkg/test/filetest.go | 5 +++++ gnovm/tests/files/addressable_5.gno | 3 +++ gnovm/tests/files/type_alias.gno | 5 +++-- gnovm/tests/files/types/eql_0f49.gno | 2 ++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/test/filetest.go b/gnovm/pkg/test/filetest.go index 1934f429568..c24c014a9ba 100644 --- a/gnovm/pkg/test/filetest.go +++ b/gnovm/pkg/test/filetest.go @@ -103,6 +103,11 @@ func (opts *TestOptions) runFiletest(filename string, source []byte) (string, er // The Error directive (and many others) will have one trailing newline, // which is not in the output - so add it there. match(errDirective, result.Error+"\n") + } else if result.Output != "" { + outputDirective := dirs.First(DirectiveOutput) + if outputDirective == nil { + return "", fmt.Errorf("unexpected output:\n%s", result.Output) + } } else { err = m.CheckEmpty() if err != nil { diff --git a/gnovm/tests/files/addressable_5.gno b/gnovm/tests/files/addressable_5.gno index 800cc744458..fa39ef42841 100644 --- a/gnovm/tests/files/addressable_5.gno +++ b/gnovm/tests/files/addressable_5.gno @@ -9,3 +9,6 @@ func main() { le := &binary.LittleEndian println(&le.AppendUint16(b, 0)[0]) } + +// Output: +// &(0 uint8) diff --git a/gnovm/tests/files/type_alias.gno b/gnovm/tests/files/type_alias.gno index e95c54126ec..09918f6d591 100644 --- a/gnovm/tests/files/type_alias.gno +++ b/gnovm/tests/files/type_alias.gno @@ -6,7 +6,8 @@ import "gno.land/p/demo/uassert" type TestingT = uassert.TestingT func main() { - println(TestingT) + println("ok") } -// No need for output; not panicking is passing. +// Output: +// ok diff --git a/gnovm/tests/files/types/eql_0f49.gno b/gnovm/tests/files/types/eql_0f49.gno index b5a4bf4ed05..b4d6f7e3972 100644 --- a/gnovm/tests/files/types/eql_0f49.gno +++ b/gnovm/tests/files/types/eql_0f49.gno @@ -14,6 +14,8 @@ func main() { } +// Output: +// true // true // true // true From dcd3834890d3f68543f423614c67f9396017c352 Mon Sep 17 00:00:00 2001 From: SunSpirit <48086732+sunspirit99@users.noreply.github.com> Date: Fri, 7 Feb 2025 00:58:08 +0700 Subject: [PATCH 37/60] feat(examples): Implement markdown package (#2912) From https://github.com/gnolang/gno/issues/2753 I keep this PR open to see if I am on the right approach. If it's suitable, I'll investigate to make further improvements and provide implementation examples in the `Render()` functions of some current demo realms to demonstrate the use of this package ![Screenshot from 2024-10-23 19-28-43](https://github.com/user-attachments/assets/a321f13a-01c7-432f-9f06-b02b5e86951a) cc @moul
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).
--------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: leohhhn --- examples/gno.land/p/sunspirit/md/gno.mod | 1 + examples/gno.land/p/sunspirit/md/md.gno | 179 ++++++++++++++++++ examples/gno.land/p/sunspirit/md/md_test.gno | 175 +++++++++++++++++ examples/gno.land/p/sunspirit/table/gno.mod | 1 + examples/gno.land/p/sunspirit/table/table.gno | 106 +++++++++++ .../gno.land/p/sunspirit/table/table_test.gno | 146 ++++++++++++++ examples/gno.land/r/sunspirit/home/gno.mod | 1 + examples/gno.land/r/sunspirit/home/home.gno | 34 ++++ examples/gno.land/r/sunspirit/md/gno.mod | 1 + examples/gno.land/r/sunspirit/md/md.gno | 158 ++++++++++++++++ examples/gno.land/r/sunspirit/md/md_test.gno | 13 ++ 11 files changed, 815 insertions(+) create mode 100644 examples/gno.land/p/sunspirit/md/gno.mod create mode 100644 examples/gno.land/p/sunspirit/md/md.gno create mode 100644 examples/gno.land/p/sunspirit/md/md_test.gno create mode 100644 examples/gno.land/p/sunspirit/table/gno.mod create mode 100644 examples/gno.land/p/sunspirit/table/table.gno create mode 100644 examples/gno.land/p/sunspirit/table/table_test.gno create mode 100644 examples/gno.land/r/sunspirit/home/gno.mod create mode 100644 examples/gno.land/r/sunspirit/home/home.gno create mode 100644 examples/gno.land/r/sunspirit/md/gno.mod create mode 100644 examples/gno.land/r/sunspirit/md/md.gno create mode 100644 examples/gno.land/r/sunspirit/md/md_test.gno diff --git a/examples/gno.land/p/sunspirit/md/gno.mod b/examples/gno.land/p/sunspirit/md/gno.mod new file mode 100644 index 00000000000..caee634f66f --- /dev/null +++ b/examples/gno.land/p/sunspirit/md/gno.mod @@ -0,0 +1 @@ +module gno.land/p/sunspirit/md diff --git a/examples/gno.land/p/sunspirit/md/md.gno b/examples/gno.land/p/sunspirit/md/md.gno new file mode 100644 index 00000000000..965373bee85 --- /dev/null +++ b/examples/gno.land/p/sunspirit/md/md.gno @@ -0,0 +1,179 @@ +package md + +import ( + "strings" + + "gno.land/p/demo/ufmt" +) + +// Builder helps to build a Markdown string from individual elements +type Builder struct { + elements []string +} + +// NewBuilder creates a new Builder instance +func NewBuilder() *Builder { + return &Builder{} +} + +// Add adds a Markdown element to the builder +func (m *Builder) Add(md ...string) *Builder { + m.elements = append(m.elements, md...) + return m +} + +// Render returns the final Markdown string joined with the specified separator +func (m *Builder) Render(separator string) string { + return strings.Join(m.elements, separator) +} + +// Bold returns bold text for markdown +func Bold(text string) string { + return ufmt.Sprintf("**%s**", text) +} + +// Italic returns italicized text for markdown +func Italic(text string) string { + return ufmt.Sprintf("*%s*", text) +} + +// Strikethrough returns strikethrough text for markdown +func Strikethrough(text string) string { + return ufmt.Sprintf("~~%s~~", text) +} + +// H1 returns a level 1 header for markdown +func H1(text string) string { + return ufmt.Sprintf("# %s\n", text) +} + +// H2 returns a level 2 header for markdown +func H2(text string) string { + return ufmt.Sprintf("## %s\n", text) +} + +// H3 returns a level 3 header for markdown +func H3(text string) string { + return ufmt.Sprintf("### %s\n", text) +} + +// H4 returns a level 4 header for markdown +func H4(text string) string { + return ufmt.Sprintf("#### %s\n", text) +} + +// H5 returns a level 5 header for markdown +func H5(text string) string { + return ufmt.Sprintf("##### %s\n", text) +} + +// H6 returns a level 6 header for markdown +func H6(text string) string { + return ufmt.Sprintf("###### %s\n", text) +} + +// BulletList returns an bullet list for markdown +func BulletList(items []string) string { + var sb strings.Builder + for _, item := range items { + sb.WriteString(ufmt.Sprintf("- %s\n", item)) + } + return sb.String() +} + +// OrderedList returns an ordered list for markdown +func OrderedList(items []string) string { + var sb strings.Builder + for i, item := range items { + sb.WriteString(ufmt.Sprintf("%d. %s\n", i+1, item)) + } + return sb.String() +} + +// TodoList returns a list of todo items with checkboxes for markdown +func TodoList(items []string, done []bool) string { + var sb strings.Builder + + for i, item := range items { + checkbox := " " + if done[i] { + checkbox = "x" + } + sb.WriteString(ufmt.Sprintf("- [%s] %s\n", checkbox, item)) + } + return sb.String() +} + +// Blockquote returns a blockquote for markdown +func Blockquote(text string) string { + lines := strings.Split(text, "\n") + var sb strings.Builder + for _, line := range lines { + sb.WriteString(ufmt.Sprintf("> %s\n", line)) + } + + return sb.String() +} + +// InlineCode returns inline code for markdown +func InlineCode(code string) string { + return ufmt.Sprintf("`%s`", code) +} + +// CodeBlock creates a markdown code block +func CodeBlock(content string) string { + return ufmt.Sprintf("```\n%s\n```", content) +} + +// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting +func LanguageCodeBlock(language, content string) string { + return ufmt.Sprintf("```%s\n%s\n```", language, content) +} + +// LineBreak returns the specified number of line breaks for markdown +func LineBreak(count uint) string { + if count > 0 { + return strings.Repeat("\n", int(count)+1) + } + return "" +} + +// HorizontalRule returns a horizontal rule for markdown +func HorizontalRule() string { + return "---\n" +} + +// Link returns a hyperlink for markdown +func Link(text, url string) string { + return ufmt.Sprintf("[%s](%s)", text, url) +} + +// Image returns an image for markdown +func Image(altText, url string) string { + return ufmt.Sprintf("![%s](%s)", altText, url) +} + +// Footnote returns a footnote for markdown +func Footnote(reference, text string) string { + return ufmt.Sprintf("[%s]: %s", reference, text) +} + +// Paragraph wraps the given text in a Markdown paragraph +func Paragraph(content string) string { + return ufmt.Sprintf("%s\n", content) +} + +// MdTable is an interface for table types that can be converted to Markdown format +type MdTable interface { + String() string +} + +// Table takes any MdTable implementation and returns its markdown representation +func Table(table MdTable) string { + return table.String() +} + +// EscapeMarkdown escapes special markdown characters in a string +func EscapeMarkdown(text string) string { + return ufmt.Sprintf("``%s``", text) +} diff --git a/examples/gno.land/p/sunspirit/md/md_test.gno b/examples/gno.land/p/sunspirit/md/md_test.gno new file mode 100644 index 00000000000..529cc2535bb --- /dev/null +++ b/examples/gno.land/p/sunspirit/md/md_test.gno @@ -0,0 +1,175 @@ +package md + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/sunspirit/table" +) + +func TestNewBuilder(t *testing.T) { + mdBuilder := NewBuilder() + + uassert.Equal(t, len(mdBuilder.elements), 0, "Expected 0 elements") +} + +func TestAdd(t *testing.T) { + mdBuilder := NewBuilder() + + header := H1("Hi") + body := Paragraph("This is a test") + + mdBuilder.Add(header, body) + + uassert.Equal(t, len(mdBuilder.elements), 2, "Expected 2 element") + uassert.Equal(t, mdBuilder.elements[0], header, "Expected element %s, got %s", header, mdBuilder.elements[0]) + uassert.Equal(t, mdBuilder.elements[1], body, "Expected element %s, got %s", body, mdBuilder.elements[1]) +} + +func TestRender(t *testing.T) { + mdBuilder := NewBuilder() + + header := H1("Hello") + body := Paragraph("This is a test") + + seperator := "\n" + expected := header + seperator + body + + output := mdBuilder.Add(header, body).Render(seperator) + + uassert.Equal(t, output, expected, "Expected rendered string %s, got %s", expected, output) +} + +func Test_Bold(t *testing.T) { + uassert.Equal(t, Bold("Hello"), "**Hello**") +} + +func Test_Italic(t *testing.T) { + uassert.Equal(t, Italic("Hello"), "*Hello*") +} + +func Test_Strikethrough(t *testing.T) { + uassert.Equal(t, Strikethrough("Hello"), "~~Hello~~") +} + +func Test_H1(t *testing.T) { + uassert.Equal(t, H1("Header 1"), "# Header 1\n") +} + +func Test_H2(t *testing.T) { + uassert.Equal(t, H2("Header 2"), "## Header 2\n") +} + +func Test_H3(t *testing.T) { + uassert.Equal(t, H3("Header 3"), "### Header 3\n") +} + +func Test_H4(t *testing.T) { + uassert.Equal(t, H4("Header 4"), "#### Header 4\n") +} + +func Test_H5(t *testing.T) { + uassert.Equal(t, H5("Header 5"), "##### Header 5\n") +} + +func Test_H6(t *testing.T) { + uassert.Equal(t, H6("Header 6"), "###### Header 6\n") +} + +func Test_BulletList(t *testing.T) { + items := []string{"Item 1", "Item 2", "Item 3"} + result := BulletList(items) + expected := "- Item 1\n- Item 2\n- Item 3\n" + uassert.Equal(t, result, expected) +} + +func Test_OrderedList(t *testing.T) { + items := []string{"Item 1", "Item 2", "Item 3"} + result := OrderedList(items) + expected := "1. Item 1\n2. Item 2\n3. Item 3\n" + uassert.Equal(t, result, expected) +} + +func Test_TodoList(t *testing.T) { + items := []string{"Task 1", "Task 2"} + done := []bool{true, false} + result := TodoList(items, done) + expected := "- [x] Task 1\n- [ ] Task 2\n" + uassert.Equal(t, result, expected) +} + +func Test_Blockquote(t *testing.T) { + text := "This is a blockquote.\nIt has multiple lines." + result := Blockquote(text) + expected := "> This is a blockquote.\n> It has multiple lines.\n" + uassert.Equal(t, result, expected) +} + +func Test_InlineCode(t *testing.T) { + result := InlineCode("code") + uassert.Equal(t, result, "`code`") +} + +func Test_LanguageCodeBlock(t *testing.T) { + result := LanguageCodeBlock("python", "print('Hello')") + expected := "```python\nprint('Hello')\n```" + uassert.Equal(t, result, expected) +} + +func Test_CodeBlock(t *testing.T) { + result := CodeBlock("print('Hello')") + expected := "```\nprint('Hello')\n```" + uassert.Equal(t, result, expected) +} + +func Test_LineBreak(t *testing.T) { + result := LineBreak(2) + expected := "\n\n\n" + uassert.Equal(t, result, expected) + + result = LineBreak(0) + expected = "" + uassert.Equal(t, result, expected) +} + +func Test_HorizontalRule(t *testing.T) { + result := HorizontalRule() + uassert.Equal(t, result, "---\n") +} + +func Test_Link(t *testing.T) { + result := Link("Google", "http://google.com") + uassert.Equal(t, result, "[Google](http://google.com)") +} + +func Test_Image(t *testing.T) { + result := Image("Alt text", "http://image.url") + uassert.Equal(t, result, "![Alt text](http://image.url)") +} + +func Test_Footnote(t *testing.T) { + result := Footnote("1", "This is a footnote.") + uassert.Equal(t, result, "[1]: This is a footnote.") +} + +func Test_Paragraph(t *testing.T) { + result := Paragraph("This is a paragraph.") + uassert.Equal(t, result, "This is a paragraph.\n") +} + +func Test_Table(t *testing.T) { + tb, err := table.New([]string{"Header1", "Header2"}, [][]string{ + {"Row1Col1", "Row1Col2"}, + {"Row2Col1", "Row2Col2"}, + }) + uassert.NoError(t, err) + + result := Table(tb) + expected := "| Header1 | Header2 |\n| ---|---|\n| Row1Col1 | Row1Col2 |\n| Row2Col1 | Row2Col2 |\n" + uassert.Equal(t, result, expected) +} + +func Test_EscapeMarkdown(t *testing.T) { + result := EscapeMarkdown("- This is `code`") + uassert.Equal(t, result, "``- This is `code```") +} diff --git a/examples/gno.land/p/sunspirit/table/gno.mod b/examples/gno.land/p/sunspirit/table/gno.mod new file mode 100644 index 00000000000..1814c50b25d --- /dev/null +++ b/examples/gno.land/p/sunspirit/table/gno.mod @@ -0,0 +1 @@ +module gno.land/p/sunspirit/table diff --git a/examples/gno.land/p/sunspirit/table/table.gno b/examples/gno.land/p/sunspirit/table/table.gno new file mode 100644 index 00000000000..8c27516c962 --- /dev/null +++ b/examples/gno.land/p/sunspirit/table/table.gno @@ -0,0 +1,106 @@ +package table + +import ( + "strings" + + "gno.land/p/demo/ufmt" +) + +// Table defines the structure for a markdown table +type Table struct { + header []string + rows [][]string +} + +// Validate checks if the number of columns in each row matches the number of columns in the header +func (t *Table) Validate() error { + numCols := len(t.header) + for _, row := range t.rows { + if len(row) != numCols { + return ufmt.Errorf("row %v does not match header length %d", row, numCols) + } + } + return nil +} + +// New creates a new Table instance, ensuring the header and rows match in size +func New(header []string, rows [][]string) (*Table, error) { + t := &Table{ + header: header, + rows: rows, + } + + if err := t.Validate(); err != nil { + return nil, err + } + + return t, nil +} + +// Table returns a markdown string for the given Table +func (t *Table) String() string { + if err := t.Validate(); err != nil { + panic(err) + } + + var sb strings.Builder + + sb.WriteString("| " + strings.Join(t.header, " | ") + " |\n") + sb.WriteString("| " + strings.Repeat("---|", len(t.header)) + "\n") + + for _, row := range t.rows { + sb.WriteString("| " + strings.Join(row, " | ") + " |\n") + } + + return sb.String() +} + +// AddRow adds a new row to the table +func (t *Table) AddRow(row []string) error { + if len(row) != len(t.header) { + return ufmt.Errorf("row %v does not match header length %d", row, len(t.header)) + } + t.rows = append(t.rows, row) + return nil +} + +// AddColumn adds a new column to the table with the specified values +func (t *Table) AddColumn(header string, values []string) error { + if len(values) != len(t.rows) { + return ufmt.Errorf("values length %d does not match the number of rows %d", len(values), len(t.rows)) + } + + // Add the new header + t.header = append(t.header, header) + + // Add the new column values to each row + for i, value := range values { + t.rows[i] = append(t.rows[i], value) + } + return nil +} + +// RemoveRow removes a row from the table by its index +func (t *Table) RemoveRow(index int) error { + if index < 0 || index >= len(t.rows) { + return ufmt.Errorf("index %d is out of range", index) + } + t.rows = append(t.rows[:index], t.rows[index+1:]...) + return nil +} + +// RemoveColumn removes a column from the table by its index +func (t *Table) RemoveColumn(index int) error { + if index < 0 || index >= len(t.header) { + return ufmt.Errorf("index %d is out of range", index) + } + + // Remove the column from the header + t.header = append(t.header[:index], t.header[index+1:]...) + + // Remove the corresponding column from each row + for i := range t.rows { + t.rows[i] = append(t.rows[i][:index], t.rows[i][index+1:]...) + } + return nil +} diff --git a/examples/gno.land/p/sunspirit/table/table_test.gno b/examples/gno.land/p/sunspirit/table/table_test.gno new file mode 100644 index 00000000000..d4cd56ad0a8 --- /dev/null +++ b/examples/gno.land/p/sunspirit/table/table_test.gno @@ -0,0 +1,146 @@ +package table + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNew(t *testing.T) { + header := []string{"Name", "Age", "Country"} + rows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25", "UK"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + uassert.Equal(t, len(header), len(table.header)) + uassert.Equal(t, len(rows), len(table.rows)) +} + +func Test_AddRow(t *testing.T) { + header := []string{"Name", "Age"} + rows := [][]string{ + {"Alice", "30"}, + {"Bob", "25"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + // Add a valid row + err = table.AddRow([]string{"Charlie", "28"}) + urequire.NoError(t, err) + + expectedRows := [][]string{ + {"Alice", "30"}, + {"Bob", "25"}, + {"Charlie", "28"}, + } + uassert.Equal(t, len(expectedRows), len(table.rows)) + + // Attempt to add a row with a different number of columns + err = table.AddRow([]string{"David"}) + uassert.Error(t, err) +} + +func Test_AddColumn(t *testing.T) { + header := []string{"Name", "Age"} + rows := [][]string{ + {"Alice", "30"}, + {"Bob", "25"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + // Add a valid column + err = table.AddColumn("Country", []string{"USA", "UK"}) + urequire.NoError(t, err) + + expectedHeader := []string{"Name", "Age", "Country"} + expectedRows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25", "UK"}, + } + uassert.Equal(t, len(expectedHeader), len(table.header)) + uassert.Equal(t, len(expectedRows), len(table.rows)) + + // Attempt to add a column with a different number of values + err = table.AddColumn("City", []string{"New York"}) + uassert.Error(t, err) +} + +func Test_RemoveRow(t *testing.T) { + header := []string{"Name", "Age", "Country"} + rows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25", "UK"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + // Remove the first row + err = table.RemoveRow(0) + urequire.NoError(t, err) + + expectedRows := [][]string{ + {"Bob", "25", "UK"}, + } + uassert.Equal(t, len(expectedRows), len(table.rows)) + + // Attempt to remove a row out of range + err = table.RemoveRow(5) + uassert.Error(t, err) +} + +func Test_RemoveColumn(t *testing.T) { + header := []string{"Name", "Age", "Country"} + rows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25", "UK"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + // Remove the second column (Age) + err = table.RemoveColumn(1) + urequire.NoError(t, err) + + expectedHeader := []string{"Name", "Country"} + expectedRows := [][]string{ + {"Alice", "USA"}, + {"Bob", "UK"}, + } + uassert.Equal(t, len(expectedHeader), len(table.header)) + uassert.Equal(t, len(expectedRows), len(table.rows)) + + // Attempt to remove a column out of range + err = table.RemoveColumn(5) + uassert.Error(t, err) +} + +func Test_Validate(t *testing.T) { + header := []string{"Name", "Age", "Country"} + rows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25"}, + } + + table, err := New(header, rows[:1]) + urequire.NoError(t, err) + + // Validate should pass + err = table.Validate() + urequire.NoError(t, err) + + // Add an invalid row and validate again + table.rows = append(table.rows, rows[1]) + err = table.Validate() + uassert.Error(t, err) +} diff --git a/examples/gno.land/r/sunspirit/home/gno.mod b/examples/gno.land/r/sunspirit/home/gno.mod new file mode 100644 index 00000000000..2aea0280fff --- /dev/null +++ b/examples/gno.land/r/sunspirit/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/sunspirit/home diff --git a/examples/gno.land/r/sunspirit/home/home.gno b/examples/gno.land/r/sunspirit/home/home.gno new file mode 100644 index 00000000000..fbf9709e8d4 --- /dev/null +++ b/examples/gno.land/r/sunspirit/home/home.gno @@ -0,0 +1,34 @@ +package home + +import ( + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/p/sunspirit/md" +) + +func Render(path string) string { + var sb strings.Builder + + sb.WriteString(md.H1("Sunspirit's Home") + md.LineBreak(1)) + + sb.WriteString(md.Paragraph(ufmt.Sprintf( + "Welcome to Sunspirit’s home! This is where I’ll bring %s to Gno.land, crafted with my experience and creativity.", + md.Italic(md.Bold("simple, useful dapps")), + )) + md.LineBreak(1)) + + sb.WriteString(md.Paragraph(ufmt.Sprintf( + "📚 I’ve created a Markdown rendering library at %s. Feel free to use it for your own projects!", + md.Link("gno.land/p/sunspirit/md", "/p/sunspirit/md"), + )) + md.LineBreak(1)) + + sb.WriteString(md.Paragraph("💬 I’d love to hear your feedback to help improve this library!") + md.LineBreak(1)) + + sb.WriteString(md.Paragraph(ufmt.Sprintf( + "🌐 You can check out a demo of this package in action at %s.", + md.Link("gno.land/r/sunspirit/md", "/r/sunspirit/md"), + )) + md.LineBreak(1)) + sb.WriteString(md.HorizontalRule()) + + return sb.String() +} diff --git a/examples/gno.land/r/sunspirit/md/gno.mod b/examples/gno.land/r/sunspirit/md/gno.mod new file mode 100644 index 00000000000..ff3a7c54d96 --- /dev/null +++ b/examples/gno.land/r/sunspirit/md/gno.mod @@ -0,0 +1 @@ +module gno.land/r/sunspirit/md diff --git a/examples/gno.land/r/sunspirit/md/md.gno b/examples/gno.land/r/sunspirit/md/md.gno new file mode 100644 index 00000000000..8c21ea0215c --- /dev/null +++ b/examples/gno.land/r/sunspirit/md/md.gno @@ -0,0 +1,158 @@ +package md + +import ( + "gno.land/p/sunspirit/md" + "gno.land/p/sunspirit/table" +) + +func Render(path string) string { + title := "A simple, flexible, and easy-to-use library for creating markdown documents in gno.land" + + mdBuilder := md.NewBuilder(). + Add(md.H1(md.Italic(md.Bold(title)))). + + // Bold Text section + Add( + md.H3(md.Bold("1. Bold Text")), + md.Paragraph("To make text bold, use the `md.Bold()` function:"), + md.Bold("This is bold text"), + ). + + // Italic Text section + Add( + md.H3(md.Bold("2. Italic Text")), + md.Paragraph("To make text italic, use the `md.Italic()` function:"), + md.Italic("This is italic text"), + ). + + // Strikethrough Text section + Add( + md.H3(md.Bold("3. Strikethrough Text")), + md.Paragraph("To add strikethrough, use the `md.Strikethrough()` function:"), + md.Strikethrough("This text is strikethrough"), + ). + + // Headers section + Add( + md.H3(md.Bold("4. Headers (H1 to H6)")), + md.Paragraph("You can create headers (H1 to H6) using the `md.H1()` to `md.H6()` functions:"), + md.H1("This is a level 1 header"), + md.H2("This is a level 2 header"), + md.H3("This is a level 3 header"), + md.H4("This is a level 4 header"), + md.H5("This is a level 5 header"), + md.H6("This is a level 6 header"), + ). + + // Bullet List section + Add( + md.H3(md.Bold("5. Bullet List")), + md.Paragraph("To create bullet lists, use the `md.BulletList()` function:"), + md.BulletList([]string{"Item 1", "Item 2", "Item 3"}), + ). + + // Ordered List section + Add( + md.H3(md.Bold("6. Ordered List")), + md.Paragraph("To create ordered lists, use the `md.OrderedList()` function:"), + md.OrderedList([]string{"First", "Second", "Third"}), + ). + + // Todo List section + Add( + md.H3(md.Bold("7. Todo List")), + md.Paragraph("You can create a todo list using the `md.TodoList()` function, which supports checkboxes:"), + md.TodoList([]string{"Task 1", "Task 2"}, []bool{true, false}), + ). + + // Blockquote section + Add( + md.H3(md.Bold("8. Blockquote")), + md.Paragraph("To create blockquotes, use the `md.Blockquote()` function:"), + md.Blockquote("This is a blockquote.\nIt can span multiple lines."), + ). + + // Inline Code section + Add( + md.H3(md.Bold("9. Inline Code")), + md.Paragraph("To insert inline code, use the `md.InlineCode()` function:"), + md.InlineCode("fmt.Println() // inline code"), + ). + + // Code Block section + Add( + md.H3(md.Bold("10. Code Block")), + md.Paragraph("For multi-line code blocks, use the `md.CodeBlock()` function:"), + md.CodeBlock("package main\n\nfunc main() {\n\t// Your code here\n}"), + ). + + // Horizontal Rule section + Add( + md.H3(md.Bold("11. Horizontal Rule")), + md.Paragraph("To add a horizontal rule (separator), use the `md.HorizontalRule()` function:"), + md.LineBreak(1), + md.HorizontalRule(), + ). + + // Language-specific Code Block section + Add( + md.H3(md.Bold("12. Language-specific Code Block")), + md.Paragraph("To create language-specific code blocks, use the `md.LanguageCodeBlock()` function:"), + md.LanguageCodeBlock("go", "package main\n\nfunc main() {}"), + ). + + // Hyperlink section + Add( + md.H3(md.Bold("13. Hyperlink")), + md.Paragraph("To create a hyperlink, use the `md.Link()` function:"), + md.Link("Gnoland official docs", "https://docs.gno.land"), + ). + + // Image section + Add( + md.H3(md.Bold("14. Image")), + md.Paragraph("To insert an image, use the `md.Image()` function:"), + md.LineBreak(1), + md.Image("Gnoland Logo", "https://gnolang.github.io/blog/2024-05-21_the-gnome/src/banner.png"), + ). + + // Footnote section + Add( + md.H3(md.Bold("15. Footnote")), + md.Paragraph("To create footnotes, use the `md.Footnote()` function:"), + md.LineBreak(1), + md.Footnote("1", "This is a footnote."), + ). + + // Table section + Add( + md.H3(md.Bold("16. Table")), + md.Paragraph("To create a table, use the `md.Table()` function. Here's an example of a table:"), + ) + + // Create a table using the table package + tb, _ := table.New([]string{"Feature", "Description"}, [][]string{ + {"Bold", "Make text bold using " + md.Bold("double asterisks")}, + {"Italic", "Make text italic using " + md.Italic("single asterisks")}, + {"Strikethrough", "Cross out text using " + md.Strikethrough("double tildes")}, + }) + mdBuilder.Add(md.Table(tb)) + + // Escaping Markdown section + mdBuilder.Add( + md.H3(md.Bold("17. Escaping Markdown")), + md.Paragraph("Sometimes, you need to escape special Markdown characters (like *, _, and `). Use the `md.EscapeMarkdown()` function for this:"), + ) + + // Example of escaping markdown + text := "- Escape special chars like *, _, and ` in markdown" + mdBuilder.Add( + md.H4("Text Without Escape:"), + text, + md.LineBreak(1), + md.H4("Text With Escape:"), + md.EscapeMarkdown(text), + ) + + return mdBuilder.Render(md.LineBreak(1)) +} diff --git a/examples/gno.land/r/sunspirit/md/md_test.gno b/examples/gno.land/r/sunspirit/md/md_test.gno new file mode 100644 index 00000000000..2e1ce9b9931 --- /dev/null +++ b/examples/gno.land/r/sunspirit/md/md_test.gno @@ -0,0 +1,13 @@ +package md + +import ( + "strings" + "testing" +) + +func TestRender(t *testing.T) { + output := Render("") + if !strings.Contains(output, "A simple, flexible, and easy-to-use library for creating markdown documents in gno.land") { + t.Errorf("invalid output") + } +} From de4a405487f521e57434c078efa50129170e04e1 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:16:42 +0100 Subject: [PATCH 38/60] fix(rpc): always return array reponses for batch requests (#3678) closes: #3676 Currently, when a batch request contains only a single item, our implementation returns a single response object instead of an array. **What This PR Does:** My changes update the logic so that even if the batch request includes only one item, the endpoint will still return an array containing that single response. --------- Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> --- tm2/pkg/bft/rpc/lib/server/handlers.go | 15 +++++++++++++- tm2/pkg/bft/rpc/lib/server/handlers_test.go | 23 +++++++-------------- tm2/pkg/bft/rpc/lib/server/http_server.go | 20 +++++++----------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index 9e10596a975..b91db806342 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -145,6 +145,11 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger *slog.Logger) http.H requests types.RPCRequests responses types.RPCResponses ) + + // isRPCRequestArray is used to determine if the incoming payload is an array of requests. + // This flag helps decide whether to return an array of responses (for batch requests) or a single response. + isRPCRequestArray := true + if err := json.Unmarshal(b, &requests); err != nil { // next, try to unmarshal as a single request var request types.RPCRequest @@ -153,6 +158,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger *slog.Logger) http.H return } requests = []types.RPCRequest{request} + isRPCRequestArray = false } for _, request := range requests { @@ -191,9 +197,16 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger *slog.Logger) http.H } responses = append(responses, types.NewRPCSuccessResponse(request.ID, result)) } - if len(responses) > 0 { + if len(responses) == 0 { + return + } + + if isRPCRequestArray { WriteRPCResponseArrayHTTP(w, responses) + return } + + WriteRPCResponseHTTP(w, responses[0]) } } diff --git a/tm2/pkg/bft/rpc/lib/server/handlers_test.go b/tm2/pkg/bft/rpc/lib/server/handlers_test.go index f6572be7e0a..dde2cf1e327 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers_test.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers_test.go @@ -171,6 +171,12 @@ func TestRPCNotificationInBatch(t *testing.T) { ]`, 1, }, + { + `[ + {"jsonrpc": "2.0","method":"c","id":"abc","params":["a","10"]} + ]`, + 1, + }, { `[ {"jsonrpc": "2.0","id": ""}, @@ -198,21 +204,8 @@ func TestRPCNotificationInBatch(t *testing.T) { // try to unmarshal an array first err = json.Unmarshal(blob, &responses) if err != nil { - // if we were actually expecting an array, but got an error - if tt.expectCount > 1 { - t.Errorf("#%d: expected an array, couldn't unmarshal it\nblob: %s", i, blob) - continue - } else { - // we were expecting an error here, so let's unmarshal a single response - var response types.RPCResponse - err = json.Unmarshal(blob, &response) - if err != nil { - t.Errorf("#%d: expected successful parsing of an RPCResponse\nblob: %s", i, blob) - continue - } - // have a single-element result - responses = types.RPCResponses{response} - } + t.Errorf("#%d: expected an array, couldn't unmarshal it\nblob: %s", i, blob) + continue } if tt.expectCount != len(responses) { t.Errorf("#%d: expected %d response(s), but got %d\nblob: %s", i, tt.expectCount, len(responses), blob) diff --git a/tm2/pkg/bft/rpc/lib/server/http_server.go b/tm2/pkg/bft/rpc/lib/server/http_server.go index a4e535160b5..a5cec3d5c81 100644 --- a/tm2/pkg/bft/rpc/lib/server/http_server.go +++ b/tm2/pkg/bft/rpc/lib/server/http_server.go @@ -119,18 +119,14 @@ func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { // can write arrays of responses for batched request/response interactions via // the JSON RPC. func WriteRPCResponseArrayHTTP(w http.ResponseWriter, res types.RPCResponses) { - if len(res) == 1 { - WriteRPCResponseHTTP(w, res[0]) - } else { - jsonBytes, err := json.MarshalIndent(res, "", " ") - if err != nil { - panic(err) - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - if _, err := w.Write(jsonBytes); err != nil { - panic(err) - } + jsonBytes, err := json.MarshalIndent(res, "", " ") + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + if _, err := w.Write(jsonBytes); err != nil { + panic(err) } } From 47395b1a95e3edf058cf823061485d8201be2258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Fri, 7 Feb 2025 02:43:04 +0100 Subject: [PATCH 39/60] fix: utilize `p2p.ExternalAddress` properly for dialing (#3581) ## Description This PR fixes a bug with the `Transport` bind address, where the bound address was taking in a wrong param, as part of an oversight. It also adds context on the newly introduced checks. The PR also fixes an issue with the redial mechanism for persistent peers, where peers wouldn't be redialed more than once. --- tm2/pkg/bft/node/node.go | 47 +++++++++++++++---- tm2/pkg/internal/p2p/p2p.go | 14 +++--- tm2/pkg/p2p/discovery/discovery.go | 23 ++++++++-- tm2/pkg/p2p/discovery/discovery_test.go | 6 +-- tm2/pkg/p2p/mock/peer.go | 2 +- tm2/pkg/p2p/peer.go | 2 +- tm2/pkg/p2p/peer_test.go | 4 +- tm2/pkg/p2p/switch.go | 61 ++++++++++++++++--------- tm2/pkg/p2p/switch_test.go | 2 +- tm2/pkg/p2p/transport.go | 1 + tm2/pkg/p2p/transport_test.go | 8 ++-- tm2/pkg/p2p/types/node_info.go | 35 ++++++++++---- tm2/pkg/p2p/types/node_info_test.go | 48 +++++++++++++------ 13 files changed, 177 insertions(+), 76 deletions(-) diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 08f1b3c58f9..7f16d6780c7 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -12,8 +12,6 @@ import ( "sync" "time" - goErrors "errors" - "github.com/gnolang/gno/tm2/pkg/bft/appconn" "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" "github.com/gnolang/gno/tm2/pkg/p2p/conn" @@ -604,12 +602,10 @@ func (n *Node) OnStart() error { } // Start the transport. - lAddr := n.config.P2P.ExternalAddress - if lAddr == "" { - lAddr = n.config.P2P.ListenAddress - } + // The listen address for the transport needs to be an address within reach of the machine NIC + listenAddress := p2pTypes.NetAddressString(n.nodeKey.ID(), n.config.P2P.ListenAddress) - addr, err := p2pTypes.NewNetAddressFromString(p2pTypes.NetAddressString(n.nodeKey.ID(), lAddr)) + addr, err := p2pTypes.NewNetAddressFromString(listenAddress) if err != nil { return fmt.Errorf("unable to parse network address, %w", err) } @@ -903,7 +899,7 @@ func makeNodeInfo( nodeInfo := p2pTypes.NodeInfo{ VersionSet: vset, - PeerID: nodeKey.ID(), + NetAddress: nil, // The shared address depends on the configuration Network: genDoc.ChainID, Version: version.Version, Channels: []byte{ @@ -918,13 +914,44 @@ func makeNodeInfo( }, } + // Make sure the discovery channel is shared with peers + // in case peer discovery is enabled if config.P2P.PeerExchange { nodeInfo.Channels = append(nodeInfo.Channels, discovery.Channel) } + // Grab the supplied listen address. + // This address needs to be valid, but it can be unspecified. + // If the listen address is unspecified (port / IP unbound), + // then this address cannot be used by peers for dialing + addr, err := p2pTypes.NewNetAddressFromString( + p2pTypes.NetAddressString(nodeKey.ID(), config.P2P.ListenAddress), + ) + if err != nil { + return p2pTypes.NodeInfo{}, fmt.Errorf("unable to parse network address, %w", err) + } + + // Use the transport listen address as the advertised address + nodeInfo.NetAddress = addr + + // Prepare the advertised dial address (if any) + // for the node, which other peers can use to dial + if config.P2P.ExternalAddress != "" { + addr, err = p2pTypes.NewNetAddressFromString( + p2pTypes.NetAddressString( + nodeKey.ID(), + config.P2P.ExternalAddress, + ), + ) + if err != nil { + return p2pTypes.NodeInfo{}, fmt.Errorf("invalid p2p external address: %w", err) + } + + nodeInfo.NetAddress = addr + } + // Validate the node info - err := nodeInfo.Validate() - if err != nil && !goErrors.Is(err, p2pTypes.ErrUnspecifiedIP) { + if err := nodeInfo.Validate(); err != nil { return p2pTypes.NodeInfo{}, fmt.Errorf("unable to validate node info, %w", err) } diff --git a/tm2/pkg/internal/p2p/p2p.go b/tm2/pkg/internal/p2p/p2p.go index 1e650e0cd25..0c8f1529b85 100644 --- a/tm2/pkg/internal/p2p/p2p.go +++ b/tm2/pkg/internal/p2p/p2p.go @@ -70,12 +70,12 @@ func MakeConnectedPeers( VersionSet: versionset.VersionSet{ versionset.VersionInfo{Name: "p2p", Version: "v0.0.0"}, }, - PeerID: key.ID(), - Network: "testing", - Software: "p2ptest", - Version: "v1.2.3-rc.0-deadbeef", - Channels: cfg.Channels, - Moniker: fmt.Sprintf("node-%d", index), + NetAddress: addr, + Network: "testing", + Software: "p2ptest", + Version: "v1.2.3-rc.0-deadbeef", + Channels: cfg.Channels, + Moniker: fmt.Sprintf("node-%d", index), Other: p2pTypes.NodeInfoOther{ TxIndex: "off", RPCAddress: fmt.Sprintf("127.0.0.1:%d", 0), @@ -231,7 +231,7 @@ func (mp *Peer) TrySend(_ byte, _ []byte) bool { return true } func (mp *Peer) Send(_ byte, _ []byte) bool { return true } func (mp *Peer) NodeInfo() p2pTypes.NodeInfo { return p2pTypes.NodeInfo{ - PeerID: mp.id, + NetAddress: mp.addr, } } func (mp *Peer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} } diff --git a/tm2/pkg/p2p/discovery/discovery.go b/tm2/pkg/p2p/discovery/discovery.go index d884b118c75..7a9da3726c0 100644 --- a/tm2/pkg/p2p/discovery/discovery.go +++ b/tm2/pkg/p2p/discovery/discovery.go @@ -160,7 +160,7 @@ func (r *Reactor) Receive(chID byte, peer p2p.PeerConn, msgBytes []byte) { // Validate the message if err := msg.ValidateBasic(); err != nil { - r.Logger.Error("unable to validate discovery message", "err", err) + r.Logger.Warn("unable to validate discovery message", "err", err) return } @@ -168,7 +168,7 @@ func (r *Reactor) Receive(chID byte, peer p2p.PeerConn, msgBytes []byte) { switch msg := msg.(type) { case *Request: if err := r.handleDiscoveryRequest(peer); err != nil { - r.Logger.Error("unable to handle discovery request", "err", err) + r.Logger.Warn("unable to handle discovery request", "err", err) } case *Response: // Make the peers available for dialing on the switch @@ -186,9 +186,21 @@ func (r *Reactor) handleDiscoveryRequest(peer p2p.PeerConn) error { peers = make([]*types.NetAddress, 0, len(localPeers)) ) - // Exclude the private peers from being shared + // Exclude the private peers from being shared, + // as well as peers who are not dialable localPeers = slices.DeleteFunc(localPeers, func(p p2p.PeerConn) bool { - return p.IsPrivate() + var ( + // Private peers are peers whose information is kept private to the node + privatePeer = p.IsPrivate() + // The reason we don't validate the net address with .Routable() + // is because of legacy logic that supports local loopbacks as advertised + // peer addresses. Introducing a .Routable() constraint will filter all + // local loopback addresses shared by peers, and will cause local deployments + // (and unit test deployments) to break and require additional setup + invalidDialAddress = p.NodeInfo().DialAddress().Validate() != nil + ) + + return privatePeer || invalidDialAddress }) // Check if there is anything to share, @@ -207,7 +219,8 @@ func (r *Reactor) handleDiscoveryRequest(peer p2p.PeerConn) error { } for _, p := range localPeers { - peers = append(peers, p.SocketAddr()) + // Make sure only routable peers are shared + peers = append(peers, p.NodeInfo().DialAddress()) } // Create the response, and marshal diff --git a/tm2/pkg/p2p/discovery/discovery_test.go b/tm2/pkg/p2p/discovery/discovery_test.go index 17404e6039a..91741c648db 100644 --- a/tm2/pkg/p2p/discovery/discovery_test.go +++ b/tm2/pkg/p2p/discovery/discovery_test.go @@ -166,7 +166,7 @@ func TestReactor_DiscoveryResponse(t *testing.T) { slices.ContainsFunc(resp.Peers, func(addr *types.NetAddress) bool { for _, localP := range peers { - if localP.SocketAddr().Equals(*addr) { + if localP.NodeInfo().DialAddress().Equals(*addr) { return true } } @@ -317,7 +317,7 @@ func TestReactor_DiscoveryResponse(t *testing.T) { slices.ContainsFunc(resp.Peers, func(addr *types.NetAddress) bool { for _, localP := range peers { - if localP.SocketAddr().Equals(*addr) { + if localP.NodeInfo().DialAddress().Equals(*addr) { return true } } @@ -373,7 +373,7 @@ func TestReactor_DiscoveryResponse(t *testing.T) { peerAddrs := make([]*types.NetAddress, 0, len(peers)) for _, p := range peers { - peerAddrs = append(peerAddrs, p.SocketAddr()) + peerAddrs = append(peerAddrs, p.NodeInfo().DialAddress()) } // Prepare the message diff --git a/tm2/pkg/p2p/mock/peer.go b/tm2/pkg/p2p/mock/peer.go index e5a01952831..5be34121924 100644 --- a/tm2/pkg/p2p/mock/peer.go +++ b/tm2/pkg/p2p/mock/peer.go @@ -57,7 +57,7 @@ func GeneratePeers(t *testing.T, count int) []*Peer { }, NodeInfoFn: func() types.NodeInfo { return types.NodeInfo{ - PeerID: key.ID(), + NetAddress: addr, } }, SocketAddrFn: func() *types.NetAddress { diff --git a/tm2/pkg/p2p/peer.go b/tm2/pkg/p2p/peer.go index 135bf4b250c..dcca81ca097 100644 --- a/tm2/pkg/p2p/peer.go +++ b/tm2/pkg/p2p/peer.go @@ -160,7 +160,7 @@ func (p *peer) OnStop() { // ID returns the peer's ID - the hex encoded hash of its pubkey. func (p *peer) ID() types.ID { - return p.nodeInfo.PeerID + return p.nodeInfo.ID() } // NodeInfo returns a copy of the peer's NodeInfo. diff --git a/tm2/pkg/p2p/peer_test.go b/tm2/pkg/p2p/peer_test.go index a74ea9e96a4..75f5172ee66 100644 --- a/tm2/pkg/p2p/peer_test.go +++ b/tm2/pkg/p2p/peer_test.go @@ -243,7 +243,9 @@ func TestPeer_Properties(t *testing.T) { }, }, nodeInfo: types.NodeInfo{ - PeerID: id, + NetAddress: &types.NetAddress{ + ID: id, + }, }, connInfo: &ConnInfo{ Outbound: testCase.outbound, diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index 7784c1f3989..c96e429973e 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -407,57 +407,76 @@ func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { peersToDial = make([]*types.NetAddress, 0) ) + // Gather addresses of persistent peers that are missing or + // not already in the dial queue sw.persistentPeers.Range(func(key, value any) bool { var ( id = key.(types.ID) addr = value.(*types.NetAddress) ) - // Check if the peer is part of the peer set - // or is scheduled for dialing - if peers.Has(id) || sw.dialQueue.Has(addr) { - return true + if !peers.Has(id) && !sw.dialQueue.Has(addr) { + peersToDial = append(peersToDial, addr) } - peersToDial = append(peersToDial, addr) - return true }) if len(peersToDial) == 0 { - // No persistent peers are missing + // No persistent peers need dialing return } - // Calculate the dial items + // Prepare dial items with the appropriate backoff dialItems := make([]dial.Item, 0, len(peersToDial)) - for _, p := range peersToDial { - item := getBackoffItem(p.ID) + for _, addr := range peersToDial { + item := getBackoffItem(addr.ID) + if item == nil { - dialItem := dial.Item{ - Time: time.Now(), - Address: p, - } + // First attempt + now := time.Now() + + dialItems = append(dialItems, + dial.Item{ + Time: now, + Address: addr, + }, + ) - dialItems = append(dialItems, dialItem) - setBackoffItem(p.ID, &backoffItem{dialItem.Time, 0}) + setBackoffItem(addr.ID, &backoffItem{ + lastDialTime: now, + attempts: 0, + }) continue } - setBackoffItem(p.ID, &backoffItem{ - lastDialTime: time.Now().Add( + // Subsequent attempt: apply backoff + var ( + attempts = item.attempts + 1 + dialTime = time.Now().Add( calculateBackoff( item.attempts, time.Second, 10*time.Minute, ), - ), - attempts: item.attempts + 1, + ) + ) + + dialItems = append(dialItems, + dial.Item{ + Time: dialTime, + Address: addr, + }, + ) + + setBackoffItem(addr.ID, &backoffItem{ + lastDialTime: dialTime, + attempts: attempts, }) } - // Add the peers to the dial queue + // Add these items to the dial queue sw.dialItems(dialItems...) } diff --git a/tm2/pkg/p2p/switch_test.go b/tm2/pkg/p2p/switch_test.go index b10ab3faba5..e5f472cc28e 100644 --- a/tm2/pkg/p2p/switch_test.go +++ b/tm2/pkg/p2p/switch_test.go @@ -727,7 +727,7 @@ func TestMultiplexSwitch_DialPeers(t *testing.T) { // as the transport (node) p.NodeInfoFn = func() types.NodeInfo { return types.NodeInfo{ - PeerID: addr.ID, + NetAddress: &addr, } } diff --git a/tm2/pkg/p2p/transport.go b/tm2/pkg/p2p/transport.go index 255fa60152b..3d64a48f437 100644 --- a/tm2/pkg/p2p/transport.go +++ b/tm2/pkg/p2p/transport.go @@ -139,6 +139,7 @@ func (mt *MultiplexTransport) Close() error { } mt.cancelFn() + return mt.listener.Close() } diff --git a/tm2/pkg/p2p/transport_test.go b/tm2/pkg/p2p/transport_test.go index 5ec4efda1ad..840eb974e76 100644 --- a/tm2/pkg/p2p/transport_test.go +++ b/tm2/pkg/p2p/transport_test.go @@ -237,7 +237,7 @@ func TestMultiplexTransport_Accept(t *testing.T) { ni := types.NodeInfo{ Network: network, // common network - PeerID: id, + NetAddress: na, Version: "v1.0.0-rc.0", Moniker: fmt.Sprintf("node-%d", index), VersionSet: make(versionset.VersionSet, 0), // compatible version set @@ -317,7 +317,7 @@ func TestMultiplexTransport_Accept(t *testing.T) { ni := types.NodeInfo{ Network: chainID, - PeerID: id, + NetAddress: na, Version: "v1.0.0-rc.0", Moniker: fmt.Sprintf("node-%d", index), VersionSet: make(versionset.VersionSet, 0), // compatible version set @@ -389,7 +389,7 @@ func TestMultiplexTransport_Accept(t *testing.T) { ni := types.NodeInfo{ Network: network, // common network - PeerID: key.ID(), + NetAddress: na, Version: "v1.0.0-rc.0", Moniker: fmt.Sprintf("node-%d", index), VersionSet: make(versionset.VersionSet, 0), // compatible version set @@ -467,7 +467,7 @@ func TestMultiplexTransport_Accept(t *testing.T) { ni := types.NodeInfo{ Network: network, // common network - PeerID: key.ID(), + NetAddress: na, Version: "v1.0.0-rc.0", Moniker: fmt.Sprintf("node-%d", index), VersionSet: make(versionset.VersionSet, 0), // compatible version set diff --git a/tm2/pkg/p2p/types/node_info.go b/tm2/pkg/p2p/types/node_info.go index 8452cb43cb8..4080ff2d8aa 100644 --- a/tm2/pkg/p2p/types/node_info.go +++ b/tm2/pkg/p2p/types/node_info.go @@ -14,7 +14,6 @@ const ( ) var ( - ErrInvalidPeerID = errors.New("invalid peer ID") ErrInvalidVersion = errors.New("invalid node version") ErrInvalidMoniker = errors.New("invalid node moniker") ErrInvalidRPCAddress = errors.New("invalid node RPC address") @@ -30,8 +29,8 @@ type NodeInfo struct { // Set of protocol versions VersionSet versionset.VersionSet `json:"version_set"` - // Unique peer identifier - PeerID ID `json:"id"` + // The advertised net address of the peer + NetAddress *NetAddress `json:"net_address"` // Check compatibility. // Channels are HexBytes so easier to read as JSON @@ -54,12 +53,27 @@ type NodeInfoOther struct { // Validate checks the self-reported NodeInfo is safe. // It returns an error if there // are too many Channels, if there are any duplicate Channels, -// if the ListenAddr is malformed, or if the ListenAddr is a host name +// if the NetAddress is malformed, or if the NetAddress is a host name // that can not be resolved to some IP func (info NodeInfo) Validate() error { - // Validate the ID - if err := info.PeerID.Validate(); err != nil { - return fmt.Errorf("%w, %w", ErrInvalidPeerID, err) + // There are a few checks that need to be performed when validating + // the node info's net address: + // - the ID needs to be valid + // - the FORMAT of the net address needs to be valid + // + // The key nuance here is that the net address is not being validated + // for its "dialability", but whether it's of the correct format. + // + // Unspecified IPs are tolerated (ex. 0.0.0.0 or ::), + // because of legacy logic that assumes node info + // can have unspecified IPs (ex. no external address is set, use + // the listen address which is bound to 0.0.0.0). + // + // These types of IPs are caught during the + // real peer info sharing process, since they are undialable + _, err := NewNetAddressFromString(NetAddressString(info.NetAddress.ID, info.NetAddress.DialString())) + if err != nil { + return fmt.Errorf("invalid net address in node info, %w", err) } // Validate Version @@ -100,7 +114,12 @@ func (info NodeInfo) Validate() error { // ID returns the local node ID func (info NodeInfo) ID() ID { - return info.PeerID + return info.NetAddress.ID +} + +// DialAddress is the advertised peer dial address (share-able) +func (info NodeInfo) DialAddress() *NetAddress { + return info.NetAddress } // CompatibleWith checks if two NodeInfo are compatible with each other. diff --git a/tm2/pkg/p2p/types/node_info_test.go b/tm2/pkg/p2p/types/node_info_test.go index d03d77e608f..575d8ae5fbd 100644 --- a/tm2/pkg/p2p/types/node_info_test.go +++ b/tm2/pkg/p2p/types/node_info_test.go @@ -2,23 +2,43 @@ package types import ( "fmt" + "net" "testing" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/versionset" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNodeInfo_Validate(t *testing.T) { t.Parallel() + generateNetAddress := func() *NetAddress { + var ( + key = GenerateNodeKey() + address = "127.0.0.1:8080" + ) + + tcpAddr, err := net.ResolveTCPAddr("tcp", address) + require.NoError(t, err) + + addr, err := NewNetAddress(key.ID(), tcpAddr) + require.NoError(t, err) + + return addr + } + t.Run("invalid peer ID", func(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: "", // zero + NetAddress: &NetAddress{ + ID: "", // zero + }, } - assert.ErrorIs(t, info.Validate(), ErrInvalidPeerID) + assert.ErrorIs(t, info.Validate(), crypto.ErrZeroID) }) t.Run("invalid version", func(t *testing.T) { @@ -47,8 +67,8 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Version: testCase.version, + NetAddress: generateNetAddress(), + Version: testCase.version, } assert.ErrorIs(t, info.Validate(), ErrInvalidVersion) @@ -86,8 +106,8 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Moniker: testCase.moniker, + NetAddress: generateNetAddress(), + Moniker: testCase.moniker, } assert.ErrorIs(t, info.Validate(), ErrInvalidMoniker) @@ -121,8 +141,8 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Moniker: "valid moniker", + NetAddress: generateNetAddress(), + Moniker: "valid moniker", Other: NodeInfoOther{ RPCAddress: testCase.rpcAddress, }, @@ -162,9 +182,9 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Moniker: "valid moniker", - Channels: testCase.channels, + NetAddress: generateNetAddress(), + Moniker: "valid moniker", + Channels: testCase.channels, } assert.ErrorIs(t, info.Validate(), testCase.expectedErr) @@ -176,9 +196,9 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Moniker: "valid moniker", - Channels: []byte{10, 20, 30}, + NetAddress: generateNetAddress(), + Moniker: "valid moniker", + Channels: []byte{10, 20, 30}, Other: NodeInfoOther{ RPCAddress: "0.0.0.0:26657", }, From 6367a3eacc0a6c5c39e95e3421af4ab47c7d5d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Fri, 7 Feb 2025 09:28:05 +0100 Subject: [PATCH 40/60] feat: add support for validating tx signatures in `gnogenesis` (#3690) ## Description This PR adds support for verifying genesis transaction signatures in `gnogenesis verify`, allowing us to quickly diagnose a potential issue with a touched `genesis.json` --- .github/workflows/genesis-verify.yml | 2 +- contribs/gnogenesis/internal/verify/verify.go | 29 +++++- .../gnogenesis/internal/verify/verify_test.go | 97 +++++++++++++++++++ tm2/pkg/crypto/mock/mock.go | 2 +- 4 files changed, 126 insertions(+), 4 deletions(-) diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml index acc41cc99ad..a9e9a43e55a 100644 --- a/.github/workflows/genesis-verify.yml +++ b/.github/workflows/genesis-verify.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - testnet: [ "test5.gno.land" ] + testnet: [ ] # Currently, all active testnet deployment genesis.json are legacy runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/contribs/gnogenesis/internal/verify/verify.go b/contribs/gnogenesis/internal/verify/verify.go index 9022711ce49..c69f41cad4d 100644 --- a/contribs/gnogenesis/internal/verify/verify.go +++ b/contribs/gnogenesis/internal/verify/verify.go @@ -12,7 +12,10 @@ import ( "github.com/gnolang/gno/tm2/pkg/commands" ) -var errInvalidGenesisState = errors.New("invalid genesis state type") +var ( + errInvalidGenesisState = errors.New("invalid genesis state type") + errInvalidTxSignature = errors.New("invalid tx signature") +) type verifyCfg struct { common.Cfg @@ -60,10 +63,32 @@ func execVerify(cfg *verifyCfg, io commands.IO) error { } // Validate the initial transactions - for _, tx := range state.Txs { + for index, tx := range state.Txs { if validateErr := tx.Tx.ValidateBasic(); validateErr != nil { return fmt.Errorf("invalid transacton, %w", validateErr) } + + // Genesis txs can only be signed by 1 account. + // Basic tx validation ensures there is at least 1 signer + signer := tx.Tx.GetSignatures()[0] + + // Grab the signature bytes of the tx. + // Genesis transactions are signed with + // account number and sequence set to 0 + signBytes, err := tx.Tx.GetSignBytes(genesis.ChainID, 0, 0) + if err != nil { + return fmt.Errorf("unable to get tx signature payload, %w", err) + } + + // Verify the signature using the public key + if !signer.PubKey.VerifyBytes(signBytes, signer.Signature) { + return fmt.Errorf( + "%w #%d, by signer %s", + errInvalidTxSignature, + index, + signer.PubKey.Address(), + ) + } } // Validate the initial balances diff --git a/contribs/gnogenesis/internal/verify/verify_test.go b/contribs/gnogenesis/internal/verify/verify_test.go index 130bd5e09bc..cc80c0423de 100644 --- a/contribs/gnogenesis/internal/verify/verify_test.go +++ b/contribs/gnogenesis/internal/verify/verify_test.go @@ -8,8 +8,12 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" "github.com/gnolang/gno/tm2/pkg/crypto/mock" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -63,6 +67,99 @@ func TestGenesis_Verify(t *testing.T) { require.Error(t, cmdErr) }) + t.Run("invalid tx signature", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + + testTable := []struct { + name string + signBytesFn func(tx *std.Tx) []byte + }{ + { + name: "invalid chain ID", + signBytesFn: func(tx *std.Tx) []byte { + // Sign the transaction, but with a chain ID + // that differs from the genesis chain ID + signBytes, err := tx.GetSignBytes(g.ChainID+"wrong", 0, 0) + require.NoError(t, err) + + return signBytes + }, + }, + { + name: "invalid account params", + signBytesFn: func(tx *std.Tx) []byte { + // Sign the transaction, but with an + // account number that is not 0 + signBytes, err := tx.GetSignBytes(g.ChainID, 10, 0) + require.NoError(t, err) + + return signBytes + }, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Generate the transaction + signer := ed25519.GenPrivKey() + + sendMsg := bank.MsgSend{ + FromAddress: signer.PubKey().Address(), + ToAddress: signer.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 10)), + } + + tx := std.Tx{ + Msgs: []std.Msg{sendMsg}, + Fee: std.Fee{ + GasWanted: 1000000, + GasFee: std.NewCoin("ugnot", 20), + }, + } + + // Sign the transaction + signBytes := testCase.signBytesFn(&tx) + + signature, err := signer.Sign(signBytes) + require.NoError(t, err) + + tx.Signatures = append(tx.Signatures, std.Signature{ + PubKey: signer.PubKey(), + Signature: signature, + }) + + g.AppState = gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{}, + Txs: []gnoland.TxWithMetadata{ + { + Tx: tx, + }, + }, + } + + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := NewVerifyCmd(commands.NewTestIO()) + args := []string{ + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidTxSignature) + }) + } + }) + t.Run("invalid balances", func(t *testing.T) { t.Parallel() diff --git a/tm2/pkg/crypto/mock/mock.go b/tm2/pkg/crypto/mock/mock.go index 9ea1c5d66dc..b3fe8c5e69f 100644 --- a/tm2/pkg/crypto/mock/mock.go +++ b/tm2/pkg/crypto/mock/mock.go @@ -42,7 +42,7 @@ func (privKey PrivKeyMock) Equals(other crypto.PrivKey) bool { func GenPrivKey() PrivKeyMock { randstr := random.RandStr(12) - return PrivKeyMock([]byte(randstr)) + return []byte(randstr) } // ------------------------------------- From 4f91841910a7fac4b93017208dab3856c3c0fa24 Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Fri, 7 Feb 2025 17:36:36 +0900 Subject: [PATCH 41/60] fix(gnoweb): header links with webquery (#3671) This PR fixes an issue where gnoweb URLs contain both a query (? prefix) and a gnowebquery ($ prefix). Previously, the header links appended the gnowebquery after the query, whereas it should be placed before the query. This update ensures the correct ordering of URL components. cf issue: https://github.com/gnolang/gno/issues/3355#issuecomment-2597023818 --------- Co-authored-by: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> --- .../pkg/gnoweb/components/layout_header.go | 26 ++++--- gno.land/pkg/gnoweb/handler.go | 20 +++--- gno.land/pkg/gnoweb/{ => weburl}/url.go | 45 +++++++------ gno.land/pkg/gnoweb/{ => weburl}/url_test.go | 67 ++++++++++++++++++- 4 files changed, 117 insertions(+), 41 deletions(-) rename gno.land/pkg/gnoweb/{ => weburl}/url.go (87%) rename gno.land/pkg/gnoweb/{ => weburl}/url_test.go (87%) diff --git a/gno.land/pkg/gnoweb/components/layout_header.go b/gno.land/pkg/gnoweb/components/layout_header.go index d446212c6e0..8ab5e95b391 100644 --- a/gno.land/pkg/gnoweb/components/layout_header.go +++ b/gno.land/pkg/gnoweb/components/layout_header.go @@ -2,6 +2,8 @@ package components import ( "net/url" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/weburl" ) type HeaderLink struct { @@ -13,38 +15,44 @@ type HeaderLink struct { type HeaderData struct { RealmPath string + RealmURL weburl.GnoURL Breadcrumb BreadcrumbData - WebQuery url.Values Links []HeaderLink ChainId string Remote string } -func StaticHeaderLinks(realmPath string, webQuery url.Values) []HeaderLink { +func StaticHeaderLinks(u weburl.GnoURL) []HeaderLink { + contentURL, sourceURL, helpURL := u, u, u + contentURL.WebQuery = url.Values{} + sourceURL.WebQuery = url.Values{"source": {""}} + helpURL.WebQuery = url.Values{"help": {""}} + return []HeaderLink{ { Label: "Content", - URL: realmPath, + URL: contentURL.EncodeWebURL(), Icon: "ico-info", - IsActive: isActive(webQuery, "Content"), + IsActive: isActive(u.WebQuery, "Content"), }, { Label: "Source", - URL: realmPath + "$source", + URL: sourceURL.EncodeWebURL(), Icon: "ico-code", - IsActive: isActive(webQuery, "Source"), + IsActive: isActive(u.WebQuery, "Source"), }, { Label: "Docs", - URL: realmPath + "$help", + URL: helpURL.EncodeWebURL(), Icon: "ico-docs", - IsActive: isActive(webQuery, "Docs"), + IsActive: isActive(u.WebQuery, "Docs"), }, } } func EnrichHeaderData(data HeaderData) HeaderData { - data.Links = StaticHeaderLinks(data.RealmPath, data.WebQuery) + data.RealmPath = data.RealmURL.EncodeURL() + data.Links = StaticHeaderLinks(data.RealmURL) return data } diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index 4c1dae31261..b5ee98614f3 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/weburl" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // For error types ) @@ -111,7 +112,7 @@ func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) { // prepareIndexBodyView prepares the data and main view for the index. func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components.IndexData) (int, *components.View) { - gnourl, err := ParseGnoURL(r.URL) + gnourl, err := weburl.ParseGnoURL(r.URL) if err != nil { h.Logger.Warn("unable to parse url path", "path", r.URL.Path, "error", err) return http.StatusNotFound, components.StatusErrorComponent("invalid path") @@ -120,9 +121,8 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components breadcrumb := generateBreadcrumbPaths(gnourl) indexData.HeadData.Title = h.Static.Domain + " - " + gnourl.Path indexData.HeaderData = components.HeaderData{ - RealmPath: gnourl.Encode(EncodePath | EncodeArgs | EncodeQuery | EncodeNoEscape), Breadcrumb: breadcrumb, - WebQuery: gnourl.WebQuery, + RealmURL: *gnourl, ChainId: h.Static.ChainId, Remote: h.Static.RemoteHelp, } @@ -137,7 +137,7 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components } // GetPackageView handles package pages. -func (h *WebHandler) GetPackageView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetPackageView(gnourl *weburl.GnoURL) (int, *components.View) { // Handle Help page if gnourl.WebQuery.Has("help") { return h.GetHelpView(gnourl) @@ -157,7 +157,7 @@ func (h *WebHandler) GetPackageView(gnourl *GnoURL) (int, *components.View) { return h.GetRealmView(gnourl) } -func (h *WebHandler) GetRealmView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetRealmView(gnourl *weburl.GnoURL) (int, *components.View) { var content bytes.Buffer meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs()) @@ -181,7 +181,7 @@ func (h *WebHandler) GetRealmView(gnourl *GnoURL) (int, *components.View) { }) } -func (h *WebHandler) GetHelpView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetHelpView(gnourl *weburl.GnoURL) (int, *components.View) { fsigs, err := h.Client.Functions(gnourl.Path) if err != nil { h.Logger.Error("unable to fetch path functions", "error", err) @@ -219,7 +219,7 @@ func (h *WebHandler) GetHelpView(gnourl *GnoURL) (int, *components.View) { }) } -func (h *WebHandler) GetSourceView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetSourceView(gnourl *weburl.GnoURL) (int, *components.View) { pkgPath := gnourl.Path files, err := h.Client.Sources(pkgPath) if err != nil { @@ -262,7 +262,7 @@ func (h *WebHandler) GetSourceView(gnourl *GnoURL) (int, *components.View) { }) } -func (h *WebHandler) GetDirectoryView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetDirectoryView(gnourl *weburl.GnoURL) (int, *components.View) { pkgPath := strings.TrimSuffix(gnourl.Path, "/") files, err := h.Client.Sources(pkgPath) if err != nil { @@ -282,7 +282,7 @@ func (h *WebHandler) GetDirectoryView(gnourl *GnoURL) (int, *components.View) { }) } -func GetClientErrorStatusPage(_ *GnoURL, err error) (int, *components.View) { +func GetClientErrorStatusPage(_ *weburl.GnoURL, err error) (int, *components.View) { if err == nil { return http.StatusOK, nil } @@ -299,7 +299,7 @@ func GetClientErrorStatusPage(_ *GnoURL, err error) (int, *components.View) { } } -func generateBreadcrumbPaths(url *GnoURL) components.BreadcrumbData { +func generateBreadcrumbPaths(url *weburl.GnoURL) components.BreadcrumbData { split := strings.Split(url.Path, "/") var data components.BreadcrumbData diff --git a/gno.land/pkg/gnoweb/url.go b/gno.land/pkg/gnoweb/weburl/url.go similarity index 87% rename from gno.land/pkg/gnoweb/url.go rename to gno.land/pkg/gnoweb/weburl/url.go index 9127225d490..cbe861e9e42 100644 --- a/gno.land/pkg/gnoweb/url.go +++ b/gno.land/pkg/gnoweb/weburl/url.go @@ -1,4 +1,4 @@ -package gnoweb +package weburl import ( "errors" @@ -97,20 +97,12 @@ func (gnoURL GnoURL) Encode(encodeFlags EncodeFlag) string { if encodeFlags.Has(EncodeWebQuery) && len(gnoURL.WebQuery) > 0 { urlstr.WriteRune('$') - if noEscape { - urlstr.WriteString(NoEscapeQuery(gnoURL.WebQuery)) - } else { - urlstr.WriteString(gnoURL.WebQuery.Encode()) - } + urlstr.WriteString(EncodeValues(gnoURL.WebQuery, !noEscape)) } if encodeFlags.Has(EncodeQuery) && len(gnoURL.Query) > 0 { urlstr.WriteRune('?') - if noEscape { - urlstr.WriteString(NoEscapeQuery(gnoURL.Query)) - } else { - urlstr.WriteString(gnoURL.Query.Encode()) - } + urlstr.WriteString(EncodeValues(gnoURL.Query, !noEscape)) } return urlstr.String() @@ -140,7 +132,7 @@ func (gnoURL GnoURL) EncodeURL() string { // EncodeWebURL encodes the path, package arguments, web query, and query into a string. // This function provides the full representation of the URL. func (gnoURL GnoURL) EncodeWebURL() string { - return gnoURL.Encode(EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery) + return gnoURL.Encode(EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery | EncodeNoEscape) } // IsPure checks if the URL path represents a pure path. @@ -226,11 +218,11 @@ func ParseGnoURL(u *url.URL) (*GnoURL, error) { }, nil } -// NoEscapeQuery generates a URL-encoded query string from the given url.Values, -// without escaping the keys and values. The query parameters are sorted by key. -func NoEscapeQuery(v url.Values) string { - // Encode encodes the values into “URL encoded” form - // ("bar=baz&foo=quux") sorted by key. +// EncodeValues generates a URL-encoded query string from the given url.Values. +// This function is a modified version of Go's `url.Values.Encode()`: https://pkg.go.dev/net/url#Values.Encode +// It takes an additional `escape` boolean argument that disables escaping on keys and values. +// Additionally, if an empty string value is passed, it omits the `=` sign, resulting in `?key` instead of `?key=` to enhance URL readability. +func EncodeValues(v url.Values, escape bool) string { if len(v) == 0 { return "" } @@ -240,16 +232,29 @@ func NoEscapeQuery(v url.Values) string { keys = append(keys, k) } slices.Sort(keys) + for _, k := range keys { vs := v[k] - keyEscaped := k + keyEncoded := k + if escape { + keyEncoded = url.QueryEscape(k) + } for _, v := range vs { if buf.Len() > 0 { buf.WriteByte('&') } - buf.WriteString(keyEscaped) + buf.WriteString(keyEncoded) + + if len(v) == 0 { + continue // Skip `=` for empty values + } + buf.WriteByte('=') - buf.WriteString(v) + if escape { + buf.WriteString(url.QueryEscape(v)) + } else { + buf.WriteString(v) + } } } return buf.String() diff --git a/gno.land/pkg/gnoweb/url_test.go b/gno.land/pkg/gnoweb/weburl/url_test.go similarity index 87% rename from gno.land/pkg/gnoweb/url_test.go rename to gno.land/pkg/gnoweb/weburl/url_test.go index 7a491eaa149..682832f5b0d 100644 --- a/gno.land/pkg/gnoweb/url_test.go +++ b/gno.land/pkg/gnoweb/weburl/url_test.go @@ -1,4 +1,4 @@ -package gnoweb +package weburl import ( "net/url" @@ -301,7 +301,7 @@ func TestEncode(t *testing.T) { }, }, EncodeFlags: EncodeWebQuery | EncodeNoEscape, - Expected: "$fun$c=B$ ar&help=", + Expected: "$fun$c=B$ ar&help", }, { @@ -450,6 +450,69 @@ func TestEncode(t *testing.T) { EncodeFlags: EncodePath | EncodeArgs | EncodeQuery, Expected: "/r/demo/foo:example?hello=42", }, + + { + Name: "WebQuery with empty value", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "source": {""}, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery | EncodeNoEscape, + Expected: "/r/demo/foo$source", + }, + + { + Name: "WebQuery with nil", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "debug": nil, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery, + Expected: "/r/demo/foo$", + }, + + { + Name: "WebQuery with regular value", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "key": {"value"}, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery, + Expected: "/r/demo/foo$key=value", + }, + + { + Name: "WebQuery mixing empty and nil and filled values", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "source": {""}, + "debug": nil, + "user": {"Alice"}, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery, + Expected: "/r/demo/foo$source&user=Alice", + }, + + { + Name: "WebQuery mixing nil and filled values", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "debug": nil, + "user": {"Alice"}, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery, + Expected: "/r/demo/foo$user=Alice", + }, } for _, tc := range testCases { From b4ebf6c81dae104f2da8adaf3515f833bcd27484 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:55:30 +0100 Subject: [PATCH 42/60] feat(examples): add p/moul/cow (#3325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package `cow` provides a Copy-on-Write AVL tree implementation. I rebased my commits to make it easier to review the differences with the original `avl.Tree`. Here’s the commit with the changes: https://github.com/gnolang/gno/pull/3325/commits/dd4e91c2cade677f1cb3d9db109f7ebdf88c3659 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> --- examples/gno.land/p/moul/cow/gno.mod | 1 + examples/gno.land/p/moul/cow/node.gno | 518 ++++++++++++++ examples/gno.land/p/moul/cow/node_test.gno | 795 +++++++++++++++++++++ examples/gno.land/p/moul/cow/tree.gno | 164 +++++ examples/gno.land/p/moul/cow/tree_test.gno | 392 ++++++++++ 5 files changed, 1870 insertions(+) create mode 100644 examples/gno.land/p/moul/cow/gno.mod create mode 100644 examples/gno.land/p/moul/cow/node.gno create mode 100644 examples/gno.land/p/moul/cow/node_test.gno create mode 100644 examples/gno.land/p/moul/cow/tree.gno create mode 100644 examples/gno.land/p/moul/cow/tree_test.gno diff --git a/examples/gno.land/p/moul/cow/gno.mod b/examples/gno.land/p/moul/cow/gno.mod new file mode 100644 index 00000000000..e5dec0bc5b4 --- /dev/null +++ b/examples/gno.land/p/moul/cow/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/cow diff --git a/examples/gno.land/p/moul/cow/node.gno b/examples/gno.land/p/moul/cow/node.gno new file mode 100644 index 00000000000..0c30871d7c4 --- /dev/null +++ b/examples/gno.land/p/moul/cow/node.gno @@ -0,0 +1,518 @@ +package cow + +//---------------------------------------- +// Node + +// Node represents a node in an AVL tree. +type Node struct { + key string // key is the unique identifier for the node. + value interface{} // value is the data stored in the node. + height int8 // height is the height of the node in the tree. + size int // size is the number of nodes in the subtree rooted at this node. + leftNode *Node // leftNode is the left child of the node. + rightNode *Node // rightNode is the right child of the node. +} + +// NewNode creates a new node with the given key and value. +func NewNode(key string, value interface{}) *Node { + return &Node{ + key: key, + value: value, + height: 0, + size: 1, + } +} + +// Size returns the size of the subtree rooted at the node. +func (node *Node) Size() int { + if node == nil { + return 0 + } + return node.size +} + +// IsLeaf checks if the node is a leaf node (has no children). +func (node *Node) IsLeaf() bool { + return node.height == 0 +} + +// Key returns the key of the node. +func (node *Node) Key() string { + return node.key +} + +// Value returns the value of the node. +func (node *Node) Value() interface{} { + return node.value +} + +// _copy creates a copy of the node (excluding value). +func (node *Node) _copy() *Node { + if node.height == 0 { + panic("Why are you copying a value node?") + } + return &Node{ + key: node.key, + height: node.height, + size: node.size, + leftNode: node.leftNode, + rightNode: node.rightNode, + } +} + +// Has checks if a node with the given key exists in the subtree rooted at the node. +func (node *Node) Has(key string) (has bool) { + if node == nil { + return false + } + if node.key == key { + return true + } + if node.height == 0 { + return false + } + if key < node.key { + return node.getLeftNode().Has(key) + } + return node.getRightNode().Has(key) +} + +// Get searches for a node with the given key in the subtree rooted at the node +// and returns its index, value, and whether it exists. +func (node *Node) Get(key string) (index int, value interface{}, exists bool) { + if node == nil { + return 0, nil, false + } + + if node.height == 0 { + if node.key == key { + return 0, node.value, true + } + if node.key < key { + return 1, nil, false + } + return 0, nil, false + } + + if key < node.key { + return node.getLeftNode().Get(key) + } + + rightNode := node.getRightNode() + index, value, exists = rightNode.Get(key) + index += node.size - rightNode.size + return index, value, exists +} + +// GetByIndex retrieves the key-value pair of the node at the given index +// in the subtree rooted at the node. +func (node *Node) GetByIndex(index int) (key string, value interface{}) { + if node.height == 0 { + if index == 0 { + return node.key, node.value + } + panic("GetByIndex asked for invalid index") + } + // TODO: could improve this by storing the sizes + leftNode := node.getLeftNode() + if index < leftNode.size { + return leftNode.GetByIndex(index) + } + return node.getRightNode().GetByIndex(index - leftNode.size) +} + +// Set inserts a new node with the given key-value pair into the subtree rooted at the node, +// and returns the new root of the subtree and whether an existing node was updated. +// +// XXX consider a better way to do this... perhaps split Node from Node. +func (node *Node) Set(key string, value interface{}) (newSelf *Node, updated bool) { + if node == nil { + return NewNode(key, value), false + } + + // Always create a new node for leaf nodes + if node.height == 0 { + return node.setLeaf(key, value) + } + + // Copy the node before modifying + newNode := node._copy() + if key < node.key { + newNode.leftNode, updated = node.getLeftNode().Set(key, value) + } else { + newNode.rightNode, updated = node.getRightNode().Set(key, value) + } + + if !updated { + newNode.calcHeightAndSize() + return newNode.balance(), updated + } + + return newNode, updated +} + +// setLeaf inserts a new leaf node with the given key-value pair into the subtree rooted at the node, +// and returns the new root of the subtree and whether an existing node was updated. +func (node *Node) setLeaf(key string, value interface{}) (newSelf *Node, updated bool) { + if key == node.key { + return NewNode(key, value), true + } + + if key < node.key { + return &Node{ + key: node.key, + height: 1, + size: 2, + leftNode: NewNode(key, value), + rightNode: node, + }, false + } + + return &Node{ + key: key, + height: 1, + size: 2, + leftNode: node, + rightNode: NewNode(key, value), + }, false +} + +// Remove deletes the node with the given key from the subtree rooted at the node. +// returns the new root of the subtree, the new leftmost leaf key (if changed), +// the removed value and the removal was successful. +func (node *Node) Remove(key string) ( + newNode *Node, newKey string, value interface{}, removed bool, +) { + if node == nil { + return nil, "", nil, false + } + if node.height == 0 { + if key == node.key { + return nil, "", node.value, true + } + return node, "", nil, false + } + if key < node.key { + var newLeftNode *Node + newLeftNode, newKey, value, removed = node.getLeftNode().Remove(key) + if !removed { + return node, "", value, false + } + if newLeftNode == nil { // left node held value, was removed + return node.rightNode, node.key, value, true + } + node = node._copy() + node.leftNode = newLeftNode + node.calcHeightAndSize() + node = node.balance() + return node, newKey, value, true + } + + var newRightNode *Node + newRightNode, newKey, value, removed = node.getRightNode().Remove(key) + if !removed { + return node, "", value, false + } + if newRightNode == nil { // right node held value, was removed + return node.leftNode, "", value, true + } + node = node._copy() + node.rightNode = newRightNode + if newKey != "" { + node.key = newKey + } + node.calcHeightAndSize() + node = node.balance() + return node, "", value, true +} + +// getLeftNode returns the left child of the node. +func (node *Node) getLeftNode() *Node { + return node.leftNode +} + +// getRightNode returns the right child of the node. +func (node *Node) getRightNode() *Node { + return node.rightNode +} + +// rotateRight performs a right rotation on the node and returns the new root. +// NOTE: overwrites node +// TODO: optimize balance & rotate +func (node *Node) rotateRight() *Node { + node = node._copy() + l := node.getLeftNode() + _l := l._copy() + + _lrCached := _l.rightNode + _l.rightNode = node + node.leftNode = _lrCached + + node.calcHeightAndSize() + _l.calcHeightAndSize() + + return _l +} + +// rotateLeft performs a left rotation on the node and returns the new root. +// NOTE: overwrites node +// TODO: optimize balance & rotate +func (node *Node) rotateLeft() *Node { + node = node._copy() + r := node.getRightNode() + _r := r._copy() + + _rlCached := _r.leftNode + _r.leftNode = node + node.rightNode = _rlCached + + node.calcHeightAndSize() + _r.calcHeightAndSize() + + return _r +} + +// calcHeightAndSize updates the height and size of the node based on its children. +// NOTE: mutates height and size +func (node *Node) calcHeightAndSize() { + node.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1 + node.size = node.getLeftNode().size + node.getRightNode().size +} + +// calcBalance calculates the balance factor of the node. +func (node *Node) calcBalance() int { + return int(node.getLeftNode().height) - int(node.getRightNode().height) +} + +// balance balances the subtree rooted at the node and returns the new root. +// NOTE: assumes that node can be modified +// TODO: optimize balance & rotate +func (node *Node) balance() (newSelf *Node) { + balance := node.calcBalance() + if balance >= -1 { + return node + } + if balance > 1 { + if node.getLeftNode().calcBalance() >= 0 { + // Left Left Case + return node.rotateRight() + } + // Left Right Case + left := node.getLeftNode() + node.leftNode = left.rotateLeft() + return node.rotateRight() + } + + if node.getRightNode().calcBalance() <= 0 { + // Right Right Case + return node.rotateLeft() + } + + // Right Left Case + right := node.getRightNode() + node.rightNode = right.rotateRight() + return node.rotateLeft() +} + +// Shortcut for TraverseInRange. +func (node *Node) Iterate(start, end string, cb func(*Node) bool) bool { + return node.TraverseInRange(start, end, true, true, cb) +} + +// Shortcut for TraverseInRange. +func (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool { + return node.TraverseInRange(start, end, false, true, cb) +} + +// TraverseInRange traverses all nodes, including inner nodes. +// Start is inclusive and end is exclusive when ascending, +// Start and end are inclusive when descending. +// Empty start and empty end denote no start and no end. +// If leavesOnly is true, only visit leaf nodes. +// NOTE: To simulate an exclusive reverse traversal, +// just append 0x00 to start. +func (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool { + if node == nil { + return false + } + afterStart := (start == "" || start < node.key) + startOrAfter := (start == "" || start <= node.key) + beforeEnd := false + if ascending { + beforeEnd = (end == "" || node.key < end) + } else { + beforeEnd = (end == "" || node.key <= end) + } + + // Run callback per inner/leaf node. + stop := false + if (!node.IsLeaf() && !leavesOnly) || + (node.IsLeaf() && startOrAfter && beforeEnd) { + stop = cb(node) + if stop { + return stop + } + } + if node.IsLeaf() { + return stop + } + + if ascending { + // check lower nodes, then higher + if afterStart { + stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + if stop { + return stop + } + if beforeEnd { + stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + } else { + // check the higher nodes first + if beforeEnd { + stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + if stop { + return stop + } + if afterStart { + stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + } + + return stop +} + +// TraverseByOffset traverses all nodes, including inner nodes. +// A limit of math.MaxInt means no limit. +func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { + if node == nil { + return false + } + + // fast paths. these happen only if TraverseByOffset is called directly on a leaf. + if limit <= 0 || offset >= node.size { + return false + } + if node.IsLeaf() { + if offset > 0 { + return false + } + return cb(node) + } + + // go to the actual recursive function. + return node.traverseByOffset(offset, limit, descending, leavesOnly, cb) +} + +// TraverseByOffset traverses the subtree rooted at the node by offset and limit, +// in either ascending or descending order, and applies the callback function to each traversed node. +// If leavesOnly is true, only leaf nodes are visited. +func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { + // caller guarantees: offset < node.size; limit > 0. + if !leavesOnly { + if cb(node) { + return true + } + } + first, second := node.getLeftNode(), node.getRightNode() + if descending { + first, second = second, first + } + if first.IsLeaf() { + // either run or skip, based on offset + if offset > 0 { + offset-- + } else { + cb(first) + limit-- + if limit <= 0 { + return false + } + } + } else { + // possible cases: + // 1 the offset given skips the first node entirely + // 2 the offset skips none or part of the first node, but the limit requires some of the second node. + // 3 the offset skips none or part of the first node, and the limit stops our search on the first node. + if offset >= first.size { + offset -= first.size // 1 + } else { + if first.traverseByOffset(offset, limit, descending, leavesOnly, cb) { + return true + } + // number of leaves which could actually be called from inside + delta := first.size - offset + offset = 0 + if delta >= limit { + return true // 3 + } + limit -= delta // 2 + } + } + + // because of the caller guarantees and the way we handle the first node, + // at this point we know that limit > 0 and there must be some values in + // this second node that we include. + + // => if the second node is a leaf, it has to be included. + if second.IsLeaf() { + return cb(second) + } + // => if it is not a leaf, it will still be enough to recursively call this + // function with the updated offset and limit + return second.traverseByOffset(offset, limit, descending, leavesOnly, cb) +} + +// Only used in testing... +func (node *Node) lmd() *Node { + if node.height == 0 { + return node + } + return node.getLeftNode().lmd() +} + +// Only used in testing... +func (node *Node) rmd() *Node { + if node.height == 0 { + return node + } + return node.getRightNode().rmd() +} + +func maxInt8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +// Equal compares two nodes for structural equality. +// WARNING: This is an expensive operation that recursively traverses the entire tree structure. +// It should only be used in tests or when absolutely necessary. +func (node *Node) Equal(other *Node) bool { + // Handle nil cases + if node == nil || other == nil { + return node == other + } + + // Compare node properties + if node.key != other.key || + node.value != other.value || + node.height != other.height || + node.size != other.size { + return false + } + + // Compare children + leftEqual := (node.leftNode == nil && other.leftNode == nil) || + (node.leftNode != nil && other.leftNode != nil && node.leftNode.Equal(other.leftNode)) + if !leftEqual { + return false + } + + rightEqual := (node.rightNode == nil && other.rightNode == nil) || + (node.rightNode != nil && other.rightNode != nil && node.rightNode.Equal(other.rightNode)) + return rightEqual +} diff --git a/examples/gno.land/p/moul/cow/node_test.gno b/examples/gno.land/p/moul/cow/node_test.gno new file mode 100644 index 00000000000..c7225fe1ab0 --- /dev/null +++ b/examples/gno.land/p/moul/cow/node_test.gno @@ -0,0 +1,795 @@ +package cow + +import ( + "fmt" + "sort" + "strings" + "testing" +) + +func TestTraverseByOffset(t *testing.T) { + const testStrings = `Alfa +Alfred +Alpha +Alphabet +Beta +Beth +Book +Browser` + tt := []struct { + name string + desc bool + }{ + {"ascending", false}, + {"descending", true}, + } + + for _, tt := range tt { + t.Run(tt.name, func(t *testing.T) { + sl := strings.Split(testStrings, "\n") + + // sort a first time in the order opposite to how we'll be traversing + // the tree, to ensure that we are not just iterating through with + // insertion order. + sort.Strings(sl) + if !tt.desc { + reverseSlice(sl) + } + + r := NewNode(sl[0], nil) + for _, v := range sl[1:] { + r, _ = r.Set(v, nil) + } + + // then sort sl in the order we'll be traversing it, so that we can + // compare the result with sl. + reverseSlice(sl) + + var result []string + for i := 0; i < len(sl); i++ { + r.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + } + + if !slicesEqual(sl, result) { + t.Errorf("want %v got %v", sl, result) + } + + for l := 2; l <= len(sl); l++ { + // "slices" + for i := 0; i <= len(sl); i++ { + max := i + l + if max > len(sl) { + max = len(sl) + } + exp := sl[i:max] + actual := []string{} + + r.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool { + actual = append(actual, tr.Key()) + return false + }) + if !slicesEqual(exp, actual) { + t.Errorf("want %v got %v", exp, actual) + } + } + } + }) + } +} + +func TestHas(t *testing.T) { + tests := []struct { + name string + input []string + hasKey string + expected bool + }{ + { + "has key in non-empty tree", + []string{"C", "A", "B", "E", "D"}, + "B", + true, + }, + { + "does not have key in non-empty tree", + []string{"C", "A", "B", "E", "D"}, + "F", + false, + }, + { + "has key in single-node tree", + []string{"A"}, + "A", + true, + }, + { + "does not have key in single-node tree", + []string{"A"}, + "B", + false, + }, + { + "does not have key in empty tree", + []string{}, + "A", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + result := tree.Has(tt.hasKey) + + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestGet(t *testing.T) { + tests := []struct { + name string + input []string + getKey string + expectIdx int + expectVal interface{} + expectExists bool + }{ + { + "get existing key", + []string{"C", "A", "B", "E", "D"}, + "B", + 1, + nil, + true, + }, + { + "get non-existent key (smaller)", + []string{"C", "A", "B", "E", "D"}, + "@", + 0, + nil, + false, + }, + { + "get non-existent key (larger)", + []string{"C", "A", "B", "E", "D"}, + "F", + 5, + nil, + false, + }, + { + "get from empty tree", + []string{}, + "A", + 0, + nil, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + idx, val, exists := tree.Get(tt.getKey) + + if idx != tt.expectIdx { + t.Errorf("Expected index %d, got %d", tt.expectIdx, idx) + } + + if val != tt.expectVal { + t.Errorf("Expected value %v, got %v", tt.expectVal, val) + } + + if exists != tt.expectExists { + t.Errorf("Expected exists %t, got %t", tt.expectExists, exists) + } + }) + } +} + +func TestGetByIndex(t *testing.T) { + tests := []struct { + name string + input []string + idx int + expectKey string + expectVal interface{} + expectPanic bool + }{ + { + "get by valid index", + []string{"C", "A", "B", "E", "D"}, + 2, + "C", + nil, + false, + }, + { + "get by valid index (smallest)", + []string{"C", "A", "B", "E", "D"}, + 0, + "A", + nil, + false, + }, + { + "get by valid index (largest)", + []string{"C", "A", "B", "E", "D"}, + 4, + "E", + nil, + false, + }, + { + "get by invalid index (negative)", + []string{"C", "A", "B", "E", "D"}, + -1, + "", + nil, + true, + }, + { + "get by invalid index (out of range)", + []string{"C", "A", "B", "E", "D"}, + 5, + "", + nil, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + if tt.expectPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected a panic but didn't get one") + } + }() + } + + key, val := tree.GetByIndex(tt.idx) + + if !tt.expectPanic { + if key != tt.expectKey { + t.Errorf("Expected key %s, got %s", tt.expectKey, key) + } + + if val != tt.expectVal { + t.Errorf("Expected value %v, got %v", tt.expectVal, val) + } + } + }) + } +} + +func TestRemove(t *testing.T) { + tests := []struct { + name string + input []string + removeKey string + expected []string + }{ + { + "remove leaf node", + []string{"C", "A", "B", "D"}, + "B", + []string{"A", "C", "D"}, + }, + { + "remove node with one child", + []string{"C", "A", "B", "D"}, + "A", + []string{"B", "C", "D"}, + }, + { + "remove node with two children", + []string{"C", "A", "B", "E", "D"}, + "C", + []string{"A", "B", "D", "E"}, + }, + { + "remove root node", + []string{"C", "A", "B", "E", "D"}, + "C", + []string{"A", "B", "D", "E"}, + }, + { + "remove non-existent key", + []string{"C", "A", "B", "E", "D"}, + "F", + []string{"A", "B", "C", "D", "E"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + tree, _, _, _ = tree.Remove(tt.removeKey) + + result := make([]string, 0) + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func TestTraverse(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "empty tree", + []string{}, + []string{}, + }, + { + "single node tree", + []string{"A"}, + []string{"A"}, + }, + { + "small tree", + []string{"C", "A", "B", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "large tree", + []string{"H", "D", "L", "B", "F", "J", "N", "A", "C", "E", "G", "I", "K", "M", "O"}, + []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + t.Run("iterate", func(t *testing.T) { + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + + t.Run("ReverseIterate", func(t *testing.T) { + var result []string + tree.ReverseIterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + expected := make([]string, len(tt.expected)) + copy(expected, tt.expected) + for i, j := 0, len(expected)-1; i < j; i, j = i+1, j-1 { + expected[i], expected[j] = expected[j], expected[i] + } + if !slicesEqual(expected, result) { + t.Errorf("want %v got %v", expected, result) + } + }) + + t.Run("TraverseInRange", func(t *testing.T) { + var result []string + start, end := "C", "M" + tree.TraverseInRange(start, end, true, true, func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + expected := make([]string, 0) + for _, key := range tt.expected { + if key >= start && key < end { + expected = append(expected, key) + } + } + if !slicesEqual(expected, result) { + t.Errorf("want %v got %v", expected, result) + } + }) + }) + } +} + +func TestRotateWhenHeightDiffers(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "right rotation when left subtree is higher", + []string{"E", "C", "A", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + { + "left rotation when right subtree is higher", + []string{"A", "C", "E", "D", "F"}, + []string{"A", "C", "D", "E", "F"}, + }, + { + "left-right rotation", + []string{"E", "A", "C", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + { + "right-left rotation", + []string{"A", "E", "C", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + // perform rotation or balance + tree = tree.balance() + + // check tree structure + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func TestRotateAndBalance(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "right rotation", + []string{"A", "B", "C", "D", "E"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "left rotation", + []string{"E", "D", "C", "B", "A"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "left-right rotation", + []string{"C", "A", "E", "B", "D"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "right-left rotation", + []string{"C", "E", "A", "D", "B"}, + []string{"A", "B", "C", "D", "E"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + tree = tree.balance() + + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func slicesEqual(w1, w2 []string) bool { + if len(w1) != len(w2) { + return false + } + for i := 0; i < len(w1); i++ { + if w1[0] != w2[0] { + return false + } + } + return true +} + +func maxint8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +func reverseSlice(ss []string) { + for i := 0; i < len(ss)/2; i++ { + j := len(ss) - 1 - i + ss[i], ss[j] = ss[j], ss[i] + } +} + +func TestNodeStructuralSharing(t *testing.T) { + t.Run("unmodified paths remain shared", func(t *testing.T) { + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + originalRight := root.rightNode + newRoot, _ := root.Set("A", 10) + + if newRoot.rightNode != originalRight { + t.Error("Unmodified right subtree should remain shared") + } + }) + + t.Run("multiple modifications reuse shared structure", func(t *testing.T) { + // Create initial tree + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + // Store original nodes + originalRight := root.rightNode + + // First modification + mod1, _ := root.Set("A", 10) + + // Second modification + mod2, _ := mod1.Set("C", 30) + + // Check sharing in first modification + if mod1.rightNode != originalRight { + t.Error("First modification should share unmodified right subtree") + } + + // Check that second modification creates new right node + if mod2.rightNode == originalRight { + t.Error("Second modification should create new right node") + } + }) +} + +func TestNodeCopyOnWrite(t *testing.T) { + t.Run("copy preserves structure", func(t *testing.T) { + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + // Only copy non-leaf nodes + if !root.IsLeaf() { + copied := root._copy() + if copied == root { + t.Error("Copy should create new instance") + } + + // Create temporary trees to use Equal method + original := &Tree{node: root} + copiedTree := &Tree{node: copied} + if !original.Equal(copiedTree) { + t.Error("Copied node should preserve structure") + } + } + }) + + t.Run("removal copy pattern", func(t *testing.T) { + // Create a more complex tree to test removal + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + root, _ = root.Set("D", 4) // Add this to ensure proper tree structure + + // Store references to original nodes + originalRight := root.rightNode + originalRightRight := originalRight.rightNode + + // Remove "A" which should only affect the left subtree + newRoot, _, _, _ := root.Remove("A") + + // Verify right subtree remains unchanged and shared + if newRoot.rightNode != originalRight { + t.Error("Right subtree should remain shared during removal of left node") + } + + // Also verify deeper nodes remain shared + if newRoot.rightNode.rightNode != originalRightRight { + t.Error("Deep right subtree should remain shared during removal") + } + + // Verify original tree is unchanged + if _, _, exists := root.Get("A"); !exists { + t.Error("Original tree should remain unchanged") + } + }) + + t.Run("copy leaf node panic", func(t *testing.T) { + leaf := NewNode("A", 1) + + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when copying leaf node") + } + }() + + // This should panic with our specific message + leaf._copy() + }) +} + +func TestNodeEqual(t *testing.T) { + tests := []struct { + name string + node1 func() *Node + node2 func() *Node + expected bool + }{ + { + name: "nil nodes", + node1: func() *Node { return nil }, + node2: func() *Node { return nil }, + expected: true, + }, + { + name: "one nil node", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return nil }, + expected: false, + }, + { + name: "single leaf nodes equal", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("A", 1) }, + expected: true, + }, + { + name: "single leaf nodes different key", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("B", 1) }, + expected: false, + }, + { + name: "single leaf nodes different value", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("A", 2) }, + expected: false, + }, + { + name: "complex trees equal", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + node2: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + expected: true, + }, + { + name: "complex trees different structure", + node1: func() *Node { + // Create a tree with structure: + // B + // / \ + // A D + n := NewNode("B", 2) + n, _ = n.Set("A", 1) + n, _ = n.Set("D", 4) + return n + }, + node2: func() *Node { + // Create a tree with structure: + // C + // / \ + // A D + n := NewNode("C", 3) + n, _ = n.Set("A", 1) + n, _ = n.Set("D", 4) + return n + }, + expected: false, // These trees should be different + }, + { + name: "complex trees same structure despite different insertion order", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + node2: func() *Node { + n, _ := NewNode("A", 1).Set("B", 2) + n, _ = n.Set("C", 3) + return n + }, + expected: true, + }, + { + name: "truly different structures", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + return n // Tree with just two nodes + }, + node2: func() *Node { + n, _ := NewNode("B", 2).Set("C", 3) + return n // Different two-node tree + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node1 := tt.node1() + node2 := tt.node2() + result := node1.Equal(node2) + if result != tt.expected { + t.Errorf("Expected Equal to return %v for %s", tt.expected, tt.name) + println("\nComparison failed:") + println("Tree 1:") + printTree(node1, 0) + println("Tree 2:") + printTree(node2, 0) + } + }) + } +} + +// Helper function to print tree structure +func printTree(node *Node, level int) { + if node == nil { + return + } + indent := strings.Repeat(" ", level) + println(fmt.Sprintf("%sKey: %s, Value: %v, Height: %d, Size: %d", + indent, node.key, node.value, node.height, node.size)) + printTree(node.leftNode, level+1) + printTree(node.rightNode, level+1) +} diff --git a/examples/gno.land/p/moul/cow/tree.gno b/examples/gno.land/p/moul/cow/tree.gno new file mode 100644 index 00000000000..befd0a414f6 --- /dev/null +++ b/examples/gno.land/p/moul/cow/tree.gno @@ -0,0 +1,164 @@ +// Package cow provides a Copy-on-Write (CoW) AVL tree implementation. +// This is a fork of gno.land/p/demo/avl that adds CoW functionality +// while maintaining the original AVL tree interface and properties. +// +// Copy-on-Write creates a copy of a data structure only when it is modified, +// while still presenting the appearance of a full copy. When a tree is cloned, +// it initially shares all its nodes with the original tree. Only when a +// modification is made to either the original or the clone are new nodes created, +// and only along the path from the root to the modified node. +// +// Key features: +// - O(1) cloning operation +// - Minimal memory usage through structural sharing +// - Full AVL tree functionality (self-balancing, ordered operations) +// - Thread-safe for concurrent reads of shared structures +// +// While the CoW mechanism handles structural copying automatically, users need +// to consider how to handle the values stored in the tree: +// +// 1. Simple Values (int, string, etc.): +// - These are copied by value automatically +// - No additional handling needed +// +// 2. Complex Values (structs, pointers): +// - Only the reference is copied by default +// - Users must implement their own deep copy mechanism if needed +// +// Example: +// +// // Create original tree +// original := cow.NewTree() +// original.Set("key1", "value1") +// +// // Create a clone - O(1) operation +// clone := original.Clone() +// +// // Modify clone - only affected nodes are copied +// clone.Set("key1", "modified") +// +// // Original remains unchanged +// val, _ := original.Get("key1") // Returns "value1" +package cow + +type IterCbFn func(key string, value interface{}) bool + +//---------------------------------------- +// Tree + +// The zero struct can be used as an empty tree. +type Tree struct { + node *Node +} + +// NewTree creates a new empty AVL tree. +func NewTree() *Tree { + return &Tree{ + node: nil, + } +} + +// Size returns the number of key-value pair in the tree. +func (tree *Tree) Size() int { + return tree.node.Size() +} + +// Has checks whether a key exists in the tree. +// It returns true if the key exists, otherwise false. +func (tree *Tree) Has(key string) (has bool) { + return tree.node.Has(key) +} + +// Get retrieves the value associated with the given key. +// It returns the value and a boolean indicating whether the key exists. +func (tree *Tree) Get(key string) (value interface{}, exists bool) { + _, value, exists = tree.node.Get(key) + return +} + +// GetByIndex retrieves the key-value pair at the specified index in the tree. +// It returns the key and value at the given index. +func (tree *Tree) GetByIndex(index int) (key string, value interface{}) { + return tree.node.GetByIndex(index) +} + +// Set inserts a key-value pair into the tree. +// If the key already exists, the value will be updated. +// It returns a boolean indicating whether the key was newly inserted or updated. +func (tree *Tree) Set(key string, value interface{}) (updated bool) { + newnode, updated := tree.node.Set(key, value) + tree.node = newnode + return updated +} + +// Remove removes a key-value pair from the tree. +// It returns the removed value and a boolean indicating whether the key was found and removed. +func (tree *Tree) Remove(key string) (value interface{}, removed bool) { + newnode, _, value, removed := tree.node.Remove(key) + tree.node = newnode + return value, removed +} + +// Iterate performs an in-order traversal of the tree within the specified key range. +// It calls the provided callback function for each key-value pair encountered. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) Iterate(start, end string, cb IterCbFn) bool { + return tree.node.TraverseInRange(start, end, true, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range. +// It calls the provided callback function for each key-value pair encountered. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) ReverseIterate(start, end string, cb IterCbFn) bool { + return tree.node.TraverseInRange(start, end, false, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// IterateByOffset performs an in-order traversal of the tree starting from the specified offset. +// It calls the provided callback function for each key-value pair encountered, up to the specified count. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) IterateByOffset(offset int, count int, cb IterCbFn) bool { + return tree.node.TraverseByOffset(offset, count, true, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset. +// It calls the provided callback function for each key-value pair encountered, up to the specified count. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool { + return tree.node.TraverseByOffset(offset, count, false, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// Equal returns true if the two trees contain the same key-value pairs. +// WARNING: This is an expensive operation that recursively traverses the entire tree structure. +// It should only be used in tests or when absolutely necessary. +func (tree *Tree) Equal(other *Tree) bool { + if tree == nil || other == nil { + return tree == other + } + return tree.node.Equal(other.node) +} + +// Clone creates a shallow copy of the tree +func (tree *Tree) Clone() *Tree { + if tree == nil { + return nil + } + return &Tree{ + node: tree.node, + } +} diff --git a/examples/gno.land/p/moul/cow/tree_test.gno b/examples/gno.land/p/moul/cow/tree_test.gno new file mode 100644 index 00000000000..6ee816455b8 --- /dev/null +++ b/examples/gno.land/p/moul/cow/tree_test.gno @@ -0,0 +1,392 @@ +package cow + +import ( + "testing" +) + +func TestNewTree(t *testing.T) { + tree := NewTree() + if tree.node != nil { + t.Error("Expected tree.node to be nil") + } +} + +func TestTreeSize(t *testing.T) { + tree := NewTree() + if tree.Size() != 0 { + t.Error("Expected empty tree size to be 0") + } + + tree.Set("key1", "value1") + tree.Set("key2", "value2") + if tree.Size() != 2 { + t.Error("Expected tree size to be 2") + } +} + +func TestTreeHas(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + if !tree.Has("key1") { + t.Error("Expected tree to have key1") + } + + if tree.Has("key2") { + t.Error("Expected tree to not have key2") + } +} + +func TestTreeGet(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + value, exists := tree.Get("key1") + if !exists || value != "value1" { + t.Error("Expected Get to return value1 and true") + } + + _, exists = tree.Get("key2") + if exists { + t.Error("Expected Get to return false for non-existent key") + } +} + +func TestTreeGetByIndex(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + + key, value := tree.GetByIndex(0) + if key != "key1" || value != "value1" { + t.Error("Expected GetByIndex(0) to return key1 and value1") + } + + key, value = tree.GetByIndex(1) + if key != "key2" || value != "value2" { + t.Error("Expected GetByIndex(1) to return key2 and value2") + } + + defer func() { + if r := recover(); r == nil { + t.Error("Expected GetByIndex to panic for out-of-range index") + } + }() + tree.GetByIndex(2) +} + +func TestTreeRemove(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + value, removed := tree.Remove("key1") + if !removed || value != "value1" || tree.Size() != 0 { + t.Error("Expected Remove to remove key-value pair") + } + + _, removed = tree.Remove("key2") + if removed { + t.Error("Expected Remove to return false for non-existent key") + } +} + +func TestTreeIterate(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.Iterate("", "", func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key1", "key2", "key3"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeReverseIterate(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.ReverseIterate("", "", func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key3", "key2", "key1"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeIterateByOffset(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.IterateByOffset(1, 2, func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key2", "key3"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeReverseIterateByOffset(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.ReverseIterateByOffset(1, 2, func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key2", "key1"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +// Verify that Tree implements avl.ITree +// var _ avl.ITree = (*Tree)(nil) // TODO: fix gnovm bug: ./examples/gno.land/p/moul/cow: test pkg: panic: gno.land/p/moul/cow/tree_test.gno:166:5: name avl not defined in fileset with files [node.gno tree.gno node_test.gno tree_test.gno]: + +func TestCopyOnWrite(t *testing.T) { + // Create original tree + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + original.Set("C", 3) + + // Create a clone + clone := original.Clone() + + // Modify clone + clone.Set("B", 20) + clone.Set("D", 4) + + // Verify original is unchanged + if val, _ := original.Get("B"); val != 2 { + t.Errorf("Original tree was modified: expected B=2, got B=%v", val) + } + if original.Has("D") { + t.Error("Original tree was modified: found key D") + } + + // Verify clone has new values + if val, _ := clone.Get("B"); val != 20 { + t.Errorf("Clone not updated: expected B=20, got B=%v", val) + } + if val, _ := clone.Get("D"); val != 4 { + t.Errorf("Clone not updated: expected D=4, got D=%v", val) + } +} + +func TestCopyOnWriteEdgeCases(t *testing.T) { + t.Run("nil tree clone", func(t *testing.T) { + var original *Tree + clone := original.Clone() + if clone != nil { + t.Error("Expected nil clone from nil tree") + } + }) + + t.Run("empty tree clone", func(t *testing.T) { + original := NewTree() + clone := original.Clone() + + // Modify clone + clone.Set("A", 1) + + if original.Size() != 0 { + t.Error("Original empty tree was modified") + } + if clone.Size() != 1 { + t.Error("Clone was not modified") + } + }) + + t.Run("multiple clones", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + + // Create multiple clones + clone1 := original.Clone() + clone2 := original.Clone() + clone3 := clone1.Clone() + + // Modify each clone differently + clone1.Set("A", 10) + clone2.Set("B", 20) + clone3.Set("C", 30) + + // Check original remains unchanged + if val, _ := original.Get("A"); val != 1 { + t.Errorf("Original modified: expected A=1, got A=%v", val) + } + if val, _ := original.Get("B"); val != 2 { + t.Errorf("Original modified: expected B=2, got B=%v", val) + } + + // Verify each clone has correct values + if val, _ := clone1.Get("A"); val != 10 { + t.Errorf("Clone1 incorrect: expected A=10, got A=%v", val) + } + if val, _ := clone2.Get("B"); val != 20 { + t.Errorf("Clone2 incorrect: expected B=20, got B=%v", val) + } + if val, _ := clone3.Get("C"); val != 30 { + t.Errorf("Clone3 incorrect: expected C=30, got C=%v", val) + } + }) + + t.Run("clone after removal", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + original.Set("C", 3) + + // Remove a node and then clone + original.Remove("B") + clone := original.Clone() + + // Modify clone + clone.Set("B", 20) + + // Verify original state + if original.Has("B") { + t.Error("Original tree should not have key B") + } + + // Verify clone state + if val, _ := clone.Get("B"); val != 20 { + t.Errorf("Clone incorrect: expected B=20, got B=%v", val) + } + }) + + t.Run("concurrent modifications", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + + clone1 := original.Clone() + clone2 := original.Clone() + + // Modify same key in different clones + clone1.Set("B", 20) + clone2.Set("B", 30) + + // Each clone should have its own value + if val, _ := clone1.Get("B"); val != 20 { + t.Errorf("Clone1 incorrect: expected B=20, got B=%v", val) + } + if val, _ := clone2.Get("B"); val != 30 { + t.Errorf("Clone2 incorrect: expected B=30, got B=%v", val) + } + }) + + t.Run("deep tree modifications", func(t *testing.T) { + original := NewTree() + // Create a deeper tree + keys := []string{"M", "F", "T", "B", "H", "P", "Z"} + for _, k := range keys { + original.Set(k, k) + } + + clone := original.Clone() + + // Modify a deep node + clone.Set("H", "modified") + + // Check original remains unchanged + if val, _ := original.Get("H"); val != "H" { + t.Errorf("Original modified: expected H='H', got H=%v", val) + } + + // Verify clone modification + if val, _ := clone.Get("H"); val != "modified" { + t.Errorf("Clone incorrect: expected H='modified', got H=%v", val) + } + }) + + t.Run("rebalancing test", func(t *testing.T) { + original := NewTree() + // Insert nodes that will cause rotations + keys := []string{"A", "B", "C", "D", "E"} + for _, k := range keys { + original.Set(k, k) + } + + clone := original.Clone() + + // Add more nodes to clone to trigger rebalancing + clone.Set("F", "F") + clone.Set("G", "G") + + // Verify original structure remains unchanged + originalKeys := collectKeys(original) + expectedOriginal := []string{"A", "B", "C", "D", "E"} + if !slicesEqual(originalKeys, expectedOriginal) { + t.Errorf("Original tree structure changed: got %v, want %v", originalKeys, expectedOriginal) + } + + // Verify clone has all keys + cloneKeys := collectKeys(clone) + expectedClone := []string{"A", "B", "C", "D", "E", "F", "G"} + if !slicesEqual(cloneKeys, expectedClone) { + t.Errorf("Clone tree structure incorrect: got %v, want %v", cloneKeys, expectedClone) + } + }) + + t.Run("value mutation test", func(t *testing.T) { + type MutableValue struct { + Data string + } + + original := NewTree() + mutable := &MutableValue{Data: "original"} + original.Set("key", mutable) + + clone := original.Clone() + + // Modify the mutable value + mutable.Data = "modified" + + // Both original and clone should see the modification + // because we're not deep copying values + origVal, _ := original.Get("key") + cloneVal, _ := clone.Get("key") + + if origVal.(*MutableValue).Data != "modified" { + t.Error("Original value not modified as expected") + } + if cloneVal.(*MutableValue).Data != "modified" { + t.Error("Clone value not modified as expected") + } + }) +} + +// Helper function to collect all keys in order +func collectKeys(tree *Tree) []string { + var keys []string + tree.Iterate("", "", func(key string, _ interface{}) bool { + keys = append(keys, key) + return false + }) + return keys +} From f75a77a818573c6a7c9967b2bfc9f4ab91cca1c9 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:21:23 +0100 Subject: [PATCH 43/60] chore(examples): add microposts example (#3660) Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/r/moul/microposts/README.md | 5 ++++ examples/gno.land/r/moul/microposts/gno.mod | 1 + .../r/moul/microposts/microposts_test.gno | 3 +++ examples/gno.land/r/moul/microposts/post.gno | 18 +++++++++++++ examples/gno.land/r/moul/microposts/realm.gno | 25 +++++++++++++++++++ 5 files changed, 52 insertions(+) create mode 100644 examples/gno.land/r/moul/microposts/README.md create mode 100644 examples/gno.land/r/moul/microposts/gno.mod create mode 100644 examples/gno.land/r/moul/microposts/microposts_test.gno create mode 100644 examples/gno.land/r/moul/microposts/post.gno create mode 100644 examples/gno.land/r/moul/microposts/realm.gno diff --git a/examples/gno.land/r/moul/microposts/README.md b/examples/gno.land/r/moul/microposts/README.md new file mode 100644 index 00000000000..5c7763020cd --- /dev/null +++ b/examples/gno.land/r/moul/microposts/README.md @@ -0,0 +1,5 @@ +# fork of `leon/fosdem25/microposts` + +removing optional lines to make the code more concise for slides. + +Original work here: https://gno.land/r/leon/fosdem25/microposts diff --git a/examples/gno.land/r/moul/microposts/gno.mod b/examples/gno.land/r/moul/microposts/gno.mod new file mode 100644 index 00000000000..00386f6e856 --- /dev/null +++ b/examples/gno.land/r/moul/microposts/gno.mod @@ -0,0 +1 @@ +module gno.land/r/moul/microposts diff --git a/examples/gno.land/r/moul/microposts/microposts_test.gno b/examples/gno.land/r/moul/microposts/microposts_test.gno new file mode 100644 index 00000000000..61929081e34 --- /dev/null +++ b/examples/gno.land/r/moul/microposts/microposts_test.gno @@ -0,0 +1,3 @@ +package microposts + +// empty file just to make sure that `gno test` tries to parse the implementation. diff --git a/examples/gno.land/r/moul/microposts/post.gno b/examples/gno.land/r/moul/microposts/post.gno new file mode 100644 index 00000000000..0832d8ac3c6 --- /dev/null +++ b/examples/gno.land/r/moul/microposts/post.gno @@ -0,0 +1,18 @@ +package microposts + +import ( + "std" + "time" +) + +type Post struct { + text string + author std.Address + createdAt time.Time +} + +func (p Post) String() string { + out := p.text + "\n" + out += "_" + p.createdAt.Format("02 Jan 2006, 15:04") + ", by " + p.author.String() + "_" + return out +} diff --git a/examples/gno.land/r/moul/microposts/realm.gno b/examples/gno.land/r/moul/microposts/realm.gno new file mode 100644 index 00000000000..a03b6dd958b --- /dev/null +++ b/examples/gno.land/r/moul/microposts/realm.gno @@ -0,0 +1,25 @@ +package microposts + +import ( + "std" + "strconv" + "time" +) + +var posts []*Post + +func CreatePost(text string) { + posts = append(posts, &Post{ + text: text, + author: std.PrevRealm().Addr(), // provided by env + createdAt: time.Now(), + }) +} + +func Render(_ string) string { + out := "# Posts\n" + for i := len(posts) - 1; i >= 0; i-- { + out += "### Post " + strconv.Itoa(i) + "\n" + posts[i].String() + } + return out +} From c41a056491c35cb926afbc2736657ec073d08657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Fri, 7 Feb 2025 16:25:03 +0100 Subject: [PATCH 44/60] fix: skip failed txs in Portal Loop reset (#3699) ## Description This PR sets the Portal Loop to skip backing up and looping failed transactions during a loop run. --- misc/loop/cmd/snapshotter.go | 8 ++++++-- misc/loop/go.mod | 2 +- misc/loop/go.sum | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/misc/loop/cmd/snapshotter.go b/misc/loop/cmd/snapshotter.go index 0173f9aad03..eef4be36d2a 100644 --- a/misc/loop/cmd/snapshotter.go +++ b/misc/loop/cmd/snapshotter.go @@ -18,7 +18,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/gnolang/tx-archive/backup" - "github.com/gnolang/tx-archive/backup/client/http" + "github.com/gnolang/tx-archive/backup/client/rpc" "github.com/gnolang/tx-archive/backup/writer/standard" ) @@ -202,6 +202,10 @@ func (s snapshotter) backupTXs(ctx context.Context, rpcURL string) error { cfg.FromBlock = 1 cfg.Watch = false + // We want to skip failed txs on the Portal Loop reset, + // because they might (unexpectedly) succeed + cfg.SkipFailedTx = true + instanceBackupFile, err := os.Create(s.instanceBackupFile) if err != nil { return err @@ -211,7 +215,7 @@ func (s snapshotter) backupTXs(ctx context.Context, rpcURL string) error { w := standard.NewWriter(instanceBackupFile) // Create the tx-archive backup service - c, err := http.NewClient(rpcURL) + c, err := rpc.NewHTTPClient(rpcURL) if err != nil { return fmt.Errorf("could not create tx-archive client, %w", err) } diff --git a/misc/loop/go.mod b/misc/loop/go.mod index c72101c7c1e..34a25043916 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -8,7 +8,7 @@ require ( github.com/docker/docker v25.0.6+incompatible github.com/docker/go-connections v0.4.0 github.com/gnolang/gno v0.1.0-nightly.20240627 - github.com/gnolang/tx-archive v0.4.2 + github.com/gnolang/tx-archive v0.5.0 github.com/prometheus/client_golang v1.17.0 github.com/sirupsen/logrus v1.9.3 ) diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 634dbdac082..56698812723 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -70,8 +70,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/tx-archive v0.4.2 h1:xBBqLLKY9riv9yxpQgVhItCWxIji2rX6xNFmCY1cEOQ= -github.com/gnolang/tx-archive v0.4.2/go.mod h1:AGUBGO+DCLuKL80a1GJRnpcJ5gxVd9L4jEJXQB9uXp4= +github.com/gnolang/tx-archive v0.5.0 h1:npM+TfM3ufF2nz1V6hq+RLkCklPbADRZXBjiyPxXVu4= +github.com/gnolang/tx-archive v0.5.0/go.mod h1:thbXpyYT57ITGABl3hH4ftLSdO8eXaPFPi5hl6jZ2UE= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From be61d7d4e1ba6c1ec271960bca378a95714232d3 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Fri, 7 Feb 2025 18:44:20 +0100 Subject: [PATCH 45/60] perf(tm2/pkg/amino): reduce RAM heavy-handedness by *bytes.Buffer pooled reuse (#3489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change comes from an analysis of a bunch of RAM and CPU profiles and noticing that realm storage needs to invoke amino.MustMarshalAny but that in the profile for TestStdlibs, it was consuming 1.28GB. ```shell ROUTINE ======================== github.com/gnolang/gno/tm2/pkg/amino.MustMarshalAny in /Users/emmanuelodeke/go/src/github.com/gnolang/gno/tm2/pkg/amino/amino.go 0 1.28GB (flat, cum) 0.61% of Total . . 80:func MustMarshalAny(o interface{}) []byte { . 1.28GB 81: return gcdc.MustMarshalAny(o) . . 82:} . . 83: . . 84:func MarshalAnySized(o interface{}) ([]byte, error) { . . 85: return gcdc.MarshalAnySized(o) . . 86:} ``` and ```shell focus=MarshalAny Showing nodes accounting for 1303.02MB, 0.6% of 217023.96MB total Dropped 13 nodes (cum <= 1085.12MB) ----------------------------------------------------------+------------- flat flat% sum% cum cum% calls calls% + context ----------------------------------------------------------+------------- 539.49MB 100% | bytes.(*Buffer).grow 539.49MB 0.25% 0.25% 539.49MB 0.25% | bytes.growSlice ----------------------------------------------------------+------------- 706.50MB 100% | bytes.(*Buffer).Write 167.01MB 0.077% 0.33% 706.50MB 0.33% | bytes.(*Buffer).grow 539.49MB 76.36% | bytes.growSlice ----------------------------------------------------------+------------- 93MB 58.68% | github.com/gnolang/gno/tm2/pkg/amino.(*Codec).encodeReflectBinaryInterface (inline) 56.50MB 35.65% | github.com/gnolang/gno/tm2/pkg/amino.(*Codec).encodeReflectBinaryStruct (inline) 9MB 5.68% | github.com/gnolang/gno/tm2/pkg/amino.(*Codec).encodeReflectBinaryList (inline) 158.51MB 0.073% 0.4% 158.51MB 0.073% | bytes.NewBuffer ----------------------------------------------------------+------------- 145.01MB 57.77% | github.com/gnolang/gno/tm2/pkg/amino.(*Codec).writeFieldIfNotEmpty 86MB 34.26% | github.com/gnolang/gno/tm2/pkg/amino.(*Codec).encodeReflectBinaryInterface 20MB 7.97% | github.com/gnolang/gno/tm2/pkg/amino.(*Codec).encodeReflectBinaryList 85.50MB 0.039% 0.44% 251.01MB 0.12% | github.com/gnolang/gno/tm2/pkg/amino.encodeFieldNumberAndTyp3 165.51MB 65.94% | bytes.(*Buffer).Write ----------------------------------------------------------+------------- 77.01MB 100% | github.com/gnolang/gno/tm2/pkg/amino.EncodeByteSlice 61.50MB 0.028% 0.47% 77.01MB 0.035% | github.com/gnolang/gno/tm2/pkg/amino.EncodeUvarint 15.51MB 20.14% | bytes.(*Buffer).Write ----------------------------------------------------------+------------- ``` but after this change, we see more than 560MB shaved off ```shell ROUTINE ======================== github.com/gnolang/gno/tm2/pkg/amino.MustMarshalAny in /Users/emmanuelodeke/go/src/github.com/gnolang/gno/tm2/pkg/amino/amino.go 0 560.95MB (flat, cum) 0.26% of Total . . 80:func MustMarshalAny(o interface{}) []byte { . 560.95MB 81: return gcdc.MustMarshalAny(o) . . 82:} . . 83: . . 84:func MarshalAnySized(o interface{}) ([]byte, error) { . . 85: return gcdc.MarshalAnySized(o) . . 86:} ``` and ```shell ----------------------------------------------------------+------------- 16.35MB 52.46% | github.com/gnolang/gno/tm2/pkg/amino.EncodeByteSlice 14.81MB 47.54% | github.com/gnolang/gno/tm2/pkg/amino.writeMaybeBare 0 0% 0.26% 31.16MB 0.014% | bytes.(*Buffer).Write 31.16MB 100% | bytes.(*Buffer).grow ----------------------------------------------------------+------------- 31.16MB 100% | bytes.(*Buffer).Write 0 0% 0.26% 31.16MB 0.014% | bytes.(*Buffer).grow 31.16MB 100% | bytes.growSlice ----------------------------------------------------------+------------- ``` and even more after the change on ensuring that tm2/pkg/amino benchmarks could run we have quite good improvements! Running out of RAM is much worse than a couple of microseconds so we can tolerate an increase in some CPU time benchmarks. ```shell name old time/op new time/op delta Binary/EmptyStruct:encode-8 3.86µs ± 5% 3.92µs ± 5% ~ (p=0.548 n=5+5) Binary/EmptyStruct:decode-8 3.79µs ± 5% 3.79µs ± 6% ~ (p=0.690 n=5+5) Binary/PrimitivesStruct:encode-8 35.5µs ± 2% 36.5µs ± 5% ~ (p=0.151 n=5+5) Binary/PrimitivesStruct:decode-8 35.0µs ± 2% 38.6µs ±11% +10.17% (p=0.016 n=5+5) Binary/ShortArraysStruct:encode-8 5.91µs ± 6% 6.36µs ± 8% +7.61% (p=0.032 n=5+5) Binary/ShortArraysStruct:decode-8 6.07µs ±21% 6.39µs ± 8% ~ (p=0.151 n=5+5) Binary/ArraysStruct:encode-8 95.1µs ± 8% 100.6µs ± 7% ~ (p=0.222 n=5+5) Binary/ArraysStruct:decode-8 91.3µs ± 5% 98.5µs ±12% ~ (p=0.222 n=5+5) Binary/ArraysArraysStruct:encode-8 131µs ± 3% 132µs ± 6% ~ (p=0.841 n=5+5) Binary/ArraysArraysStruct:decode-8 136µs ± 9% 134µs ± 3% ~ (p=0.548 n=5+5) Binary/SlicesStruct:encode-8 85.4µs ± 1% 92.3µs ± 9% +8.15% (p=0.008 n=5+5) Binary/SlicesStruct:decode-8 87.1µs ± 8% 94.8µs ± 7% ~ (p=0.056 n=5+5) Binary/SlicesSlicesStruct:encode-8 506µs ± 2% 545µs ± 9% ~ (p=0.151 n=5+5) Binary/SlicesSlicesStruct:decode-8 506µs ± 3% 523µs ± 3% ~ (p=0.095 n=5+5) Binary/PointersStruct:encode-8 56.8µs ± 4% 65.5µs ±20% +15.43% (p=0.016 n=5+5) Binary/PointersStruct:decode-8 57.5µs ± 3% 55.9µs ± 3% ~ (p=0.095 n=5+5) Binary/PointerSlicesStruct:encode-8 162µs ± 4% 172µs ±21% ~ (p=0.841 n=5+5) Binary/PointerSlicesStruct:decode-8 163µs ± 5% 185µs ±13% ~ (p=0.095 n=5+5) Binary/ComplexSt:encode-8 314µs ± 3% 354µs ±11% +12.90% (p=0.008 n=5+5) Binary/ComplexSt:decode-8 319µs ± 2% 338µs ± 4% +5.87% (p=0.008 n=5+5) Binary/EmbeddedSt1:encode-8 39.8µs ± 7% 39.3µs ± 8% ~ (p=1.000 n=5+5) Binary/EmbeddedSt1:decode-8 37.0µs ± 4% 37.8µs ± 6% ~ (p=0.690 n=5+5) Binary/EmbeddedSt2:encode-8 316µs ± 7% 307µs ± 3% ~ (p=0.222 n=5+5) Binary/EmbeddedSt2:decode-8 316µs ± 3% 306µs ± 2% ~ (p=0.095 n=5+5) Binary/EmbeddedSt3:encode-8 217µs ± 7% 201µs ± 1% -7.26% (p=0.008 n=5+5) Binary/EmbeddedSt3:decode-8 222µs ±10% 204µs ± 2% -8.50% (p=0.032 n=5+5) Binary/EmbeddedSt4:encode-8 332µs ± 4% 325µs ± 3% ~ (p=0.421 n=5+5) Binary/EmbeddedSt4:decode-8 332µs ± 4% 324µs ± 5% ~ (p=0.095 n=5+5) Binary/EmbeddedSt5:encode-8 218µs ± 2% 212µs ± 3% ~ (p=0.056 n=5+5) Binary/EmbeddedSt5:decode-8 224µs ± 8% 209µs ± 1% -6.85% (p=0.008 n=5+5) Binary/AminoMarshalerStruct1:encode-8 9.03µs ± 6% 8.97µs ±12% ~ (p=0.841 n=5+5) Binary/AminoMarshalerStruct1:decode-8 8.91µs ± 5% 8.81µs ± 4% ~ (p=0.841 n=5+5) Binary/AminoMarshalerStruct2:encode-8 13.2µs ±10% 12.2µs ± 2% -7.26% (p=0.008 n=5+5) Binary/AminoMarshalerStruct2:decode-8 13.2µs ± 6% 12.5µs ± 5% ~ (p=0.095 n=5+5) Binary/AminoMarshalerStruct3:encode-8 7.17µs ± 3% 7.50µs ± 8% ~ (p=0.548 n=5+5) Binary/AminoMarshalerStruct3:decode-8 7.12µs ± 4% 7.84µs ±10% +10.12% (p=0.016 n=5+5) Binary/AminoMarshalerInt4:encode-8 6.60µs ± 5% 6.96µs ±11% ~ (p=0.421 n=5+5) Binary/AminoMarshalerInt4:decode-8 6.79µs ±12% 7.04µs ±15% ~ (p=0.690 n=5+5) Binary/AminoMarshalerInt5:encode-8 6.64µs ± 4% 6.92µs ± 5% +4.09% (p=0.032 n=5+5) Binary/AminoMarshalerInt5:decode-8 6.55µs ± 3% 7.76µs ±10% +18.44% (p=0.008 n=5+5) Binary/AminoMarshalerStruct6:encode-8 11.7µs ± 5% 13.2µs ±10% +13.09% (p=0.008 n=5+5) Binary/AminoMarshalerStruct6:decode-8 11.4µs ± 3% 11.6µs ± 2% ~ (p=0.222 n=5+5) Binary/AminoMarshalerStruct7:encode-8 9.86µs ± 1% 10.10µs ±19% ~ (p=0.310 n=5+5) Binary/AminoMarshalerStruct7:decode-8 9.55µs ± 3% 9.75µs ±10% ~ (p=0.690 n=5+5) name old alloc/op new alloc/op delta Binary/EmptyStruct:encode-8 1.50kB ± 0% 1.41kB ± 0% -6.32% (p=0.008 n=5+5) Binary/EmptyStruct:decode-8 1.50kB ± 0% 1.41kB ± 0% -6.32% (p=0.008 n=5+5) Binary/PrimitivesStruct:encode-8 10.4kB ± 0% 9.6kB ± 0% -7.82% (p=0.008 n=5+5) Binary/PrimitivesStruct:decode-8 10.4kB ± 0% 9.6kB ± 0% -7.82% (p=0.000 n=4+5) Binary/ShortArraysStruct:encode-8 2.11kB ± 0% 1.92kB ± 0% -9.04% (p=0.008 n=5+5) Binary/ShortArraysStruct:decode-8 2.11kB ± 0% 1.92kB ± 0% -9.04% (p=0.008 n=5+5) Binary/ArraysStruct:encode-8 25.9kB ± 0% 22.0kB ± 0% -15.04% (p=0.008 n=5+5) Binary/ArraysStruct:decode-8 25.9kB ± 0% 22.0kB ± 0% -15.04% (p=0.008 n=5+5) Binary/ArraysArraysStruct:encode-8 37.7kB ± 0% 25.3kB ± 0% -33.07% (p=0.008 n=5+5) Binary/ArraysArraysStruct:decode-8 37.7kB ± 0% 25.3kB ± 0% -33.07% (p=0.008 n=5+5) Binary/SlicesStruct:encode-8 28.2kB ± 0% 25.1kB ± 0% -10.96% (p=0.008 n=5+5) Binary/SlicesStruct:decode-8 28.2kB ± 0% 25.1kB ± 0% -10.97% (p=0.008 n=5+5) Binary/SlicesSlicesStruct:encode-8 183kB ± 0% 147kB ± 0% -19.92% (p=0.008 n=5+5) Binary/SlicesSlicesStruct:decode-8 183kB ± 0% 147kB ± 0% -19.92% (p=0.008 n=5+5) Binary/PointersStruct:encode-8 14.4kB ± 0% 13.6kB ± 0% -5.64% (p=0.008 n=5+5) Binary/PointersStruct:decode-8 14.4kB ± 0% 13.6kB ± 0% -5.64% (p=0.008 n=5+5) Binary/PointerSlicesStruct:encode-8 43.9kB ± 0% 40.2kB ± 0% -8.49% (p=0.008 n=5+5) Binary/PointerSlicesStruct:decode-8 43.9kB ± 0% 40.2kB ± 0% -8.49% (p=0.008 n=5+5) Binary/ComplexSt:encode-8 95.3kB ± 0% 78.2kB ± 0% -17.97% (p=0.008 n=5+5) Binary/ComplexSt:decode-8 95.3kB ± 0% 78.2kB ± 0% -17.97% (p=0.008 n=5+5) Binary/EmbeddedSt1:encode-8 11.3kB ± 0% 10.2kB ± 0% -9.62% (p=0.000 n=5+4) Binary/EmbeddedSt1:decode-8 11.3kB ± 0% 10.2kB ± 0% -9.61% (p=0.000 n=5+4) Binary/EmbeddedSt2:encode-8 95.5kB ± 0% 78.3kB ± 0% -17.96% (p=0.008 n=5+5) Binary/EmbeddedSt2:decode-8 95.5kB ± 0% 78.4kB ± 0% -17.94% (p=0.008 n=5+5) Binary/EmbeddedSt3:encode-8 68.3kB ± 0% 56.6kB ± 0% -17.22% (p=0.008 n=5+5) Binary/EmbeddedSt3:decode-8 68.3kB ± 0% 56.6kB ± 0% -17.21% (p=0.008 n=5+5) Binary/EmbeddedSt4:encode-8 97.2kB ± 0% 82.3kB ± 0% -15.32% (p=0.008 n=5+5) Binary/EmbeddedSt4:decode-8 97.2kB ± 0% 82.3kB ± 0% -15.31% (p=0.008 n=5+5) Binary/EmbeddedSt5:encode-8 65.9kB ± 0% 55.3kB ± 0% -16.19% (p=0.008 n=5+5) Binary/EmbeddedSt5:decode-8 66.0kB ± 0% 55.3kB ± 0% -16.18% (p=0.008 n=5+5) Binary/AminoMarshalerStruct1:encode-8 2.87kB ± 0% 2.66kB ± 0% -7.23% (p=0.008 n=5+5) Binary/AminoMarshalerStruct1:decode-8 2.87kB ± 0% 2.66kB ± 0% -7.23% (p=0.008 n=5+5) Binary/AminoMarshalerStruct2:encode-8 4.58kB ± 0% 3.62kB ± 0% -20.95% (p=0.008 n=5+5) Binary/AminoMarshalerStruct2:decode-8 4.58kB ± 0% 3.62kB ± 0% -20.95% (p=0.008 n=5+5) Binary/AminoMarshalerStruct3:encode-8 2.42kB ± 0% 2.31kB ± 0% -4.62% (p=0.008 n=5+5) Binary/AminoMarshalerStruct3:decode-8 2.42kB ± 0% 2.31kB ± 0% -4.62% (p=0.008 n=5+5) Binary/AminoMarshalerInt4:encode-8 2.38kB ± 0% 2.15kB ± 0% -9.38% (p=0.008 n=5+5) Binary/AminoMarshalerInt4:decode-8 2.38kB ± 0% 2.15kB ± 0% -9.38% (p=0.008 n=5+5) Binary/AminoMarshalerInt5:encode-8 2.36kB ± 0% 2.27kB ± 0% -4.07% (p=0.008 n=5+5) Binary/AminoMarshalerInt5:decode-8 2.36kB ± 0% 2.27kB ± 0% -4.07% (p=0.008 n=5+5) Binary/AminoMarshalerStruct6:encode-8 3.51kB ± 0% 3.19kB ± 0% -9.05% (p=0.008 n=5+5) Binary/AminoMarshalerStruct6:decode-8 3.51kB ± 0% 3.19kB ± 0% -9.05% (p=0.008 n=5+5) Binary/AminoMarshalerStruct7:encode-8 2.89kB ± 0% 2.67kB ± 0% -7.72% (p=0.008 n=5+5) Binary/AminoMarshalerStruct7:decode-8 2.89kB ± 0% 2.67kB ± 0% -7.72% (p=0.008 n=5+5) name old allocs/op new allocs/op delta Binary/EmptyStruct:encode-8 38.0 ± 0% 36.0 ± 0% -5.26% (p=0.008 n=5+5) Binary/EmptyStruct:decode-8 38.0 ± 0% 36.0 ± 0% -5.26% (p=0.008 n=5+5) Binary/PrimitivesStruct:encode-8 439 ± 0% 429 ± 0% -2.28% (p=0.008 n=5+5) Binary/PrimitivesStruct:decode-8 439 ± 0% 429 ± 0% -2.28% (p=0.008 n=5+5) Binary/ShortArraysStruct:encode-8 56.0 ± 0% 52.0 ± 0% -7.14% (p=0.008 n=5+5) Binary/ShortArraysStruct:decode-8 56.0 ± 0% 52.0 ± 0% -7.14% (p=0.008 n=5+5) Binary/ArraysStruct:encode-8 977 ± 0% 919 ± 0% -5.94% (p=0.008 n=5+5) Binary/ArraysStruct:decode-8 977 ± 0% 919 ± 0% -5.94% (p=0.008 n=5+5) Binary/ArraysArraysStruct:encode-8 1.28k ± 0% 1.08k ± 0% -15.05% (p=0.008 n=5+5) Binary/ArraysArraysStruct:decode-8 1.28k ± 0% 1.08k ± 0% -15.05% (p=0.008 n=5+5) Binary/SlicesStruct:encode-8 1.01k ± 0% 0.97k ± 0% -3.77% (p=0.008 n=5+5) Binary/SlicesStruct:decode-8 1.01k ± 0% 0.97k ± 0% -3.77% (p=0.008 n=5+5) Binary/SlicesSlicesStruct:encode-8 6.33k ± 0% 5.95k ± 0% -5.90% (p=0.008 n=5+5) Binary/SlicesSlicesStruct:decode-8 6.33k ± 0% 5.95k ± 0% -5.90% (p=0.008 n=5+5) Binary/PointersStruct:encode-8 637 ± 0% 627 ± 0% -1.57% (p=0.008 n=5+5) Binary/PointersStruct:decode-8 637 ± 0% 627 ± 0% -1.57% (p=0.008 n=5+5) Binary/PointerSlicesStruct:encode-8 1.62k ± 0% 1.56k ± 0% -3.28% (p=0.008 n=5+5) Binary/PointerSlicesStruct:decode-8 1.62k ± 0% 1.56k ± 0% -3.28% (p=0.008 n=5+5) Binary/ComplexSt:encode-8 3.37k ± 0% 3.22k ± 0% -4.62% (p=0.008 n=5+5) Binary/ComplexSt:decode-8 3.37k ± 0% 3.22k ± 0% -4.62% (p=0.008 n=5+5) Binary/EmbeddedSt1:encode-8 453 ± 0% 440 ± 0% -2.87% (p=0.008 n=5+5) Binary/EmbeddedSt1:decode-8 453 ± 0% 440 ± 0% -2.87% (p=0.008 n=5+5) Binary/EmbeddedSt2:encode-8 3.37k ± 0% 3.22k ± 0% -4.62% (p=0.008 n=5+5) Binary/EmbeddedSt2:decode-8 3.37k ± 0% 3.22k ± 0% -4.62% (p=0.008 n=5+5) Binary/EmbeddedSt3:encode-8 2.32k ± 0% 2.20k ± 0% -5.38% (p=0.008 n=5+5) Binary/EmbeddedSt3:decode-8 2.32k ± 0% 2.20k ± 0% -5.38% (p=0.008 n=5+5) Binary/EmbeddedSt4:encode-8 3.67k ± 0% 3.54k ± 0% -3.73% (p=0.008 n=5+5) Binary/EmbeddedSt4:decode-8 3.67k ± 0% 3.54k ± 0% -3.73% (p=0.008 n=5+5) Binary/EmbeddedSt5:encode-8 2.32k ± 0% 2.20k ± 0% -5.00% (p=0.008 n=5+5) Binary/EmbeddedSt5:decode-8 2.32k ± 0% 2.20k ± 0% -5.00% (p=0.008 n=5+5) Binary/AminoMarshalerStruct1:encode-8 97.0 ± 0% 94.0 ± 0% -3.09% (p=0.008 n=5+5) Binary/AminoMarshalerStruct1:decode-8 97.0 ± 0% 94.0 ± 0% -3.09% (p=0.008 n=5+5) Binary/AminoMarshalerStruct2:encode-8 149 ± 0% 133 ± 0% -10.74% (p=0.008 n=5+5) Binary/AminoMarshalerStruct2:decode-8 149 ± 0% 133 ± 0% -10.74% (p=0.008 n=5+5) Binary/AminoMarshalerStruct3:encode-8 77.0 ± 0% 76.0 ± 0% -1.30% (p=0.008 n=5+5) Binary/AminoMarshalerStruct3:decode-8 77.0 ± 0% 76.0 ± 0% -1.30% (p=0.008 n=5+5) Binary/AminoMarshalerInt4:encode-8 71.0 ± 0% 68.0 ± 0% -4.23% (p=0.008 n=5+5) Binary/AminoMarshalerInt4:decode-8 71.0 ± 0% 68.0 ± 0% -4.23% (p=0.008 n=5+5) Binary/AminoMarshalerInt5:encode-8 74.0 ± 0% 73.0 ± 0% -1.35% (p=0.008 n=5+5) Binary/AminoMarshalerInt5:decode-8 74.0 ± 0% 73.0 ± 0% -1.35% (p=0.008 n=5+5) Binary/AminoMarshalerStruct6:encode-8 122 ± 0% 117 ± 0% -4.10% (p=0.008 n=5+5) Binary/AminoMarshalerStruct6:decode-8 122 ± 0% 117 ± 0% -4.10% (p=0.008 n=5+5) Binary/AminoMarshalerStruct7:encode-8 101 ± 0% 98 ± 0% -2.97% (p=0.008 n=5+5) Binary/AminoMarshalerStruct7:decode-8 101 ± 0% 98 ± 0% -2.97% (p=0.008 n=5+5) ``` Fixes #3488 --------- Co-authored-by: Morgan Bazalgette --- contribs/gnodev/go.mod | 1 + contribs/gnodev/go.sum | 2 ++ contribs/gnofaucet/go.mod | 1 + contribs/gnofaucet/go.sum | 2 ++ contribs/gnogenesis/go.mod | 1 + contribs/gnogenesis/go.sum | 2 ++ contribs/gnohealth/go.mod | 1 + contribs/gnohealth/go.sum | 2 ++ contribs/gnokeykc/go.mod | 1 + contribs/gnokeykc/go.sum | 2 ++ contribs/gnomigrate/go.mod | 1 + contribs/gnomigrate/go.sum | 2 ++ go.mod | 1 + go.sum | 2 ++ misc/autocounterd/go.mod | 1 + misc/autocounterd/go.sum | 2 ++ misc/loop/go.mod | 1 + misc/loop/go.sum | 2 ++ tm2/pkg/amino/amino.go | 48 ++++++++++++++++++++++------------ tm2/pkg/amino/binary_encode.go | 35 +++++++++++++++++-------- tm2/pkg/amino/codec.go | 5 ++-- tm2/pkg/amino/json_encode.go | 5 ++-- tm2/pkg/amino/wellknown.go | 5 ++-- 23 files changed, 91 insertions(+), 34 deletions(-) diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index a4c106a24ee..0ad16ba9bb3 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -79,6 +79,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index e87c2de6441..f4bf32aafd5 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -226,6 +226,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index 32d2e322098..88c05e0d778 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -32,6 +32,7 @@ require ( github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index 5b3cfdc3289..e6743b75960 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -126,6 +126,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index 5b28c8774c8..8af370f8169 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -36,6 +36,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.etcd.io/bbolt v1.3.11 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index dcd853e9148..e3462f9c431 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -133,6 +133,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod index 203dac360b7..76d7cd9c437 100644 --- a/contribs/gnohealth/go.mod +++ b/contribs/gnohealth/go.mod @@ -23,6 +23,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.10.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum index e51cadf1564..3c8b5de45f2 100644 --- a/contribs/gnohealth/go.sum +++ b/contribs/gnohealth/go.sum @@ -118,6 +118,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 73e51f6b25e..3abcf3d834f 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -38,6 +38,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 7a058c85750..6b4f81dfcf5 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -139,6 +139,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index 83d88c354e7..96f6dc9bdc6 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -33,6 +33,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect go.etcd.io/bbolt v1.3.11 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index dcd853e9148..e3462f9c431 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -133,6 +133,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= diff --git a/go.mod b/go.mod index ce58b8f7998..027ba6359bc 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + github.com/valyala/bytebufferpool v1.0.0 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.etcd.io/bbolt v1.3.11 diff --git a/go.sum b/go.sum index 046d9c8c75a..5fd4cddd627 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= diff --git a/misc/autocounterd/go.mod b/misc/autocounterd/go.mod index 730a3d901b7..972975d4fb0 100644 --- a/misc/autocounterd/go.mod +++ b/misc/autocounterd/go.mod @@ -29,6 +29,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/misc/autocounterd/go.sum b/misc/autocounterd/go.sum index 3d0eae7661b..6d6d87fa01a 100644 --- a/misc/autocounterd/go.sum +++ b/misc/autocounterd/go.sum @@ -133,6 +133,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= diff --git a/misc/loop/go.mod b/misc/loop/go.mod index 34a25043916..4c5a3f41839 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -56,6 +56,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect go.etcd.io/bbolt v1.3.11 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 56698812723..c5aed820f5e 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -179,6 +179,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= diff --git a/tm2/pkg/amino/amino.go b/tm2/pkg/amino/amino.go index 262f5d9a54e..b8942c49029 100644 --- a/tm2/pkg/amino/amino.go +++ b/tm2/pkg/amino/amino.go @@ -219,7 +219,8 @@ func (cdc *Codec) MarshalSized(o interface{}) ([]byte, error) { cdc.doAutoseal() // Write the bytes here. - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) // Write the bz without length-prefixing. bz, err := cdc.Marshal(o) @@ -239,7 +240,7 @@ func (cdc *Codec) MarshalSized(o interface{}) ([]byte, error) { return nil, err } - return buf.Bytes(), nil + return copyBytes(buf.Bytes()), nil } // MarshalSizedWriter writes the bytes as would be returned from @@ -271,8 +272,8 @@ func (cdc *Codec) MarshalAnySized(o interface{}) ([]byte, error) { cdc.doAutoseal() // Write the bytes here. - buf := new(bytes.Buffer) - + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) // Write the bz without length-prefixing. bz, err := cdc.MarshalAny(o) if err != nil { @@ -291,7 +292,7 @@ func (cdc *Codec) MarshalAnySized(o interface{}) ([]byte, error) { return nil, err } - return buf.Bytes(), nil + return copyBytes(buf.Bytes()), nil } func (cdc *Codec) MustMarshalAnySized(o interface{}) []byte { @@ -357,7 +358,9 @@ func (cdc *Codec) MarshalReflect(o interface{}) ([]byte, error) { // Encode Amino:binary bytes. var bz []byte - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + rt := rv.Type() info, err := cdc.getTypeInfoWLock(rt) if err != nil { @@ -377,7 +380,7 @@ func (cdc *Codec) MarshalReflect(o interface{}) ([]byte, error) { if err = cdc.writeFieldIfNotEmpty(buf, 1, info, FieldOptions{}, FieldOptions{}, rv, writeEmpty); err != nil { return nil, err } - bz = buf.Bytes() + bz = copyBytes(buf.Bytes()) } else { // The passed in BinFieldNum is only relevant for when the type is to // be encoded unpacked (elements are Typ3_ByteLength). In that case, @@ -387,7 +390,7 @@ func (cdc *Codec) MarshalReflect(o interface{}) ([]byte, error) { if err != nil { return nil, err } - bz = buf.Bytes() + bz = copyBytes(buf.Bytes()) } // If bz is empty, prefer nil. if len(bz) == 0 { @@ -443,16 +446,23 @@ func (cdc *Codec) MarshalAny(o interface{}) ([]byte, error) { } // Encode as interface. - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) err = cdc.encodeReflectBinaryInterface(buf, iinfo, reflect.ValueOf(&ivar).Elem(), FieldOptions{}, true) if err != nil { return nil, err } - bz := buf.Bytes() + bz := copyBytes(buf.Bytes()) return bz, nil } +func copyBytes(bz []byte) []byte { + cp := make([]byte, len(bz)) + copy(cp, bz) + return cp +} + // Panics if error. func (cdc *Codec) MustMarshalAny(o interface{}) []byte { bz, err := cdc.MarshalAny(o) @@ -764,7 +774,8 @@ func (cdc *Codec) JSONMarshal(o interface{}) ([]byte, error) { return []byte("null"), nil } rt := rv.Type() - w := new(bytes.Buffer) + w := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(w) info, err := cdc.getTypeInfoWLock(rt) if err != nil { return nil, err @@ -772,7 +783,8 @@ func (cdc *Codec) JSONMarshal(o interface{}) ([]byte, error) { if err = cdc.encodeReflectJSON(w, info, rv, FieldOptions{}); err != nil { return nil, err } - return w.Bytes(), nil + + return copyBytes(w.Bytes()), nil } func (cdc *Codec) MarshalJSONAny(o interface{}) ([]byte, error) { @@ -802,12 +814,14 @@ func (cdc *Codec) MarshalJSONAny(o interface{}) ([]byte, error) { } // Encode as interface. - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + err = cdc.encodeReflectJSONInterface(buf, iinfo, reflect.ValueOf(&ivar).Elem(), FieldOptions{}) if err != nil { return nil, err } - bz := buf.Bytes() + bz := copyBytes(buf.Bytes()) return bz, nil } @@ -863,12 +877,12 @@ func (cdc *Codec) MarshalJSONIndent(o interface{}, prefix, indent string) ([]byt if err != nil { return nil, err } + var out bytes.Buffer - err = json.Indent(&out, bz, prefix, indent) - if err != nil { + if err := json.Indent(&out, bz, prefix, indent); err != nil { return nil, err } - return out.Bytes(), nil + return copyBytes(out.Bytes()), nil } // ---------------------------------------- diff --git a/tm2/pkg/amino/binary_encode.go b/tm2/pkg/amino/binary_encode.go index 426cc520604..45758329284 100644 --- a/tm2/pkg/amino/binary_encode.go +++ b/tm2/pkg/amino/binary_encode.go @@ -1,12 +1,13 @@ package amino import ( - "bytes" "encoding/binary" "errors" "fmt" "io" "reflect" + + "github.com/valyala/bytebufferpool" ) const beOptionByte = 0x01 @@ -209,6 +210,8 @@ func (cdc *Codec) encodeReflectBinary(w io.Writer, info *TypeInfo, rv reflect.Va return err } +var poolBytesBuffer = new(bytebufferpool.Pool) + func (cdc *Codec) encodeReflectBinaryInterface(w io.Writer, iinfo *TypeInfo, rv reflect.Value, fopts FieldOptions, bare bool, ) (err error) { @@ -250,7 +253,9 @@ func (cdc *Codec) encodeReflectBinaryInterface(w io.Writer, iinfo *TypeInfo, rv // For Proto3 compatibility, encode interfaces as google.protobuf.Any // Write field #1, TypeURL - buf := bytes.NewBuffer(nil) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + { fnum := uint32(1) err = encodeFieldNumberAndTyp3(buf, fnum, Typ3ByteLength) @@ -269,7 +274,9 @@ func (cdc *Codec) encodeReflectBinaryInterface(w io.Writer, iinfo *TypeInfo, rv { // google.protobuf.Any values must be a struct, or an unpacked list which // is indistinguishable from a struct. - buf2 := bytes.NewBuffer(nil) + buf2 := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf2) + if !cinfo.IsStructOrUnpacked(fopts) { writeEmpty := false // Encode with an implicit struct, with a single field with number 1. @@ -356,7 +363,8 @@ func (cdc *Codec) encodeReflectBinaryList(w io.Writer, info *TypeInfo, rv reflec // Proto3 byte-length prefixing incurs alloc cost on the encoder. // Here we incur it for unpacked form for ease of dev. - buf := bytes.NewBuffer(nil) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) // If elem is not already a ByteLength type, write in packed form. // This is a Proto wart due to Proto backwards compatibility issues. @@ -393,6 +401,9 @@ func (cdc *Codec) encodeReflectBinaryList(w io.Writer, info *TypeInfo, rv reflec einfo.Elem.ReprType.Type.Kind() != reflect.Uint8 && einfo.Elem.ReprType.GetTyp3(fopts) != Typ3ByteLength + elemBuf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(elemBuf) + // Write elems in unpacked form. for i := 0; i < rv.Len(); i++ { // Write elements as repeated fields of the parent struct. @@ -431,20 +442,21 @@ func (cdc *Codec) encodeReflectBinaryList(w io.Writer, info *TypeInfo, rv reflec // form) are represented as lists of implicit structs. if writeImplicit { // Write field key for Value field of implicit struct. - buf2 := new(bytes.Buffer) - err = encodeFieldNumberAndTyp3(buf2, 1, Typ3ByteLength) + + err = encodeFieldNumberAndTyp3(elemBuf, 1, Typ3ByteLength) if err != nil { return } // Write field value of implicit struct to buf2. efopts := fopts efopts.BinFieldNum = 0 // dontcare - err = cdc.encodeReflectBinary(buf2, einfo, derv, efopts, false, 0) + err = cdc.encodeReflectBinary(elemBuf, einfo, derv, efopts, false, 0) if err != nil { return } // Write implicit struct to buf. - err = EncodeByteSlice(buf, buf2.Bytes()) + err = EncodeByteSlice(buf, elemBuf.Bytes()) + elemBuf.Reset() if err != nil { return } @@ -497,7 +509,8 @@ func (cdc *Codec) encodeReflectBinaryStruct(w io.Writer, info *TypeInfo, rv refl // Proto3 incurs a cost in writing non-root structs. // Here we incur it for root structs as well for ease of dev. - buf := bytes.NewBuffer(nil) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) for _, field := range info.Fields { // Get type info for field. @@ -553,7 +566,7 @@ func encodeFieldNumberAndTyp3(w io.Writer, num uint32, typ Typ3) (err error) { } func (cdc *Codec) writeFieldIfNotEmpty( - buf *bytes.Buffer, + buf *bytebufferpool.ByteBuffer, fieldNum uint32, finfo *TypeInfo, structsFopts FieldOptions, // the wrapping struct's FieldOptions if any @@ -579,7 +592,7 @@ func (cdc *Codec) writeFieldIfNotEmpty( if !isWriteEmpty && lBeforeValue == lAfterValue-1 && buf.Bytes()[buf.Len()-1] == 0x00 { // rollback typ3/fieldnum and last byte if // not a pointer and empty: - buf.Truncate(lBeforeKey) + buf.Set(buf.Bytes()[:lBeforeKey]) } return nil } diff --git a/tm2/pkg/amino/codec.go b/tm2/pkg/amino/codec.go index 3fa7634e3ad..ba24f49a808 100644 --- a/tm2/pkg/amino/codec.go +++ b/tm2/pkg/amino/codec.go @@ -1,7 +1,6 @@ package amino import ( - "bytes" "fmt" "io" "reflect" @@ -113,7 +112,9 @@ func (info *TypeInfo) String() string { // before it's fully populated. return "" } - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + buf.Write([]byte("TypeInfo{")) buf.Write([]byte(fmt.Sprintf("Type:%v,", info.Type))) if info.ConcreteInfo.Registered { diff --git a/tm2/pkg/amino/json_encode.go b/tm2/pkg/amino/json_encode.go index 113c3486565..99e1b445917 100644 --- a/tm2/pkg/amino/json_encode.go +++ b/tm2/pkg/amino/json_encode.go @@ -1,7 +1,6 @@ package amino import ( - "bytes" "encoding/json" "fmt" "io" @@ -156,7 +155,9 @@ func (cdc *Codec) encodeReflectJSONInterface(w io.Writer, iinfo *TypeInfo, rv re } // Write Value to buffer - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + cdc.encodeReflectJSON(buf, cinfo, crv, fopts) value := buf.Bytes() if len(value) == 0 { diff --git a/tm2/pkg/amino/wellknown.go b/tm2/pkg/amino/wellknown.go index 7720c2894d9..4053c23e893 100644 --- a/tm2/pkg/amino/wellknown.go +++ b/tm2/pkg/amino/wellknown.go @@ -3,7 +3,6 @@ package amino // NOTE: We must not depend on protubuf libraries for serialization. import ( - "bytes" "fmt" "io" "reflect" @@ -342,7 +341,9 @@ func encodeReflectBinaryWellKnown(w io.Writer, info *TypeInfo, rv reflect.Value, } // Maybe recurse with length-prefixing. if !bare { - buf := bytes.NewBuffer(nil) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + ok, err = encodeReflectBinaryWellKnown(buf, info, rv, fopts, true) if err != nil { return false, err From 52fca76c31d88fc380227c453353f5e5910bb16b Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 9 Feb 2025 05:28:14 +0100 Subject: [PATCH 46/60] chore: fmt (#3578) for the context, i was trying to fix these unrelated PR diffs: https://github.com/gnolang/gno/pull/3176/files#diff-adbe9861bf11ff27b97aafd5a5e8bbe30dca0474c3008cd066f7fd8863b8b3d0L1, then I noticed #3577. This PR: - reformats the generated files - updates the makefiles to ensure that `make generate` will also format the generated files - updates the CI to check for both formatted and generated files - [x] Depends on #3577 - [x] Depends on #3584 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .github/workflows/build_template.yml | 3 ++- gno.land/Makefile | 6 +++++- gnovm/Makefile | 2 +- gnovm/cmd/gno/README.md | 19 ++++++++++--------- .../softfloat/runtime_softfloat64_test.go | 3 ++- gnovm/tests/stdlibs/README.md | 2 +- tm2/Makefile | 5 +++++ tm2/pkg/overflow/overflow_impl.go | 12 ++++-------- 8 files changed, 30 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build_template.yml b/.github/workflows/build_template.yml index a2c96f2d37e..454e76b84da 100644 --- a/.github/workflows/build_template.yml +++ b/.github/workflows/build_template.yml @@ -23,7 +23,8 @@ jobs: - name: Check generated files are up to date working-directory: ${{ inputs.modulepath }} run: | - go generate -x ./... + make generate + if [ "$(git status -s)" != "" ]; then echo "command 'go generate' creates file that differ from git tree, please run 'go generate' and commit:" git status -s diff --git a/gno.land/Makefile b/gno.land/Makefile index 075560f44a9..90ba7451c35 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -50,9 +50,13 @@ install.gnokey:; go install ./cmd/gnokey .PHONY: dev.gnoweb generate.gnoweb dev.gnoweb: make -C ./pkg/gnoweb dev -generate.gnoweb: + +.PHONY: generate +generate: + go generate -x ./... make -C ./pkg/gnoweb generate + .PHONY: fclean fclean: clean rm -rf gnoland-data genesis.json diff --git a/gnovm/Makefile b/gnovm/Makefile index 2206fa2c8c8..fa77045d067 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -128,7 +128,7 @@ _test.filetest:; # TODO: move _dev.stringer to go:generate instructions, simplify generate # to just go generate. .PHONY: generate -generate: _dev.stringer _dev.generate _dev.docs +generate: _dev.stringer _dev.generate _dev.docs fmt imports .PHONY: _dev.docs _dev.docs: diff --git a/gnovm/cmd/gno/README.md b/gnovm/cmd/gno/README.md index 81b45622c05..81d3de2cf62 100644 --- a/gnovm/cmd/gno/README.md +++ b/gnovm/cmd/gno/README.md @@ -14,15 +14,16 @@ USAGE gno [arguments] SUBCOMMANDS - bug start a bug report - clean remove generated and cached data - doc show documentation for package or symbol - env print gno environment information - fmt gnofmt (reformat) package sources - mod module maintenance - run run gno packages - test test packages - tool run specified gno tool + bug start a bug report + clean remove generated and cached data + doc show documentation for package or symbol + env print gno environment information + fmt gnofmt (reformat) package sources + mod module maintenance + run run gno packages + test test packages + tool run specified gno tool + version display installed gno version ``` diff --git a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go index 8b5d34650f1..70c76655a97 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go +++ b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go @@ -10,11 +10,12 @@ package softfloat_test import ( - . "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" "math" "math/rand" "runtime" "testing" + + . "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" ) // turn uint64 op into float64 op diff --git a/gnovm/tests/stdlibs/README.md b/gnovm/tests/stdlibs/README.md index 8742447e59a..d264cf35c45 100644 --- a/gnovm/tests/stdlibs/README.md +++ b/gnovm/tests/stdlibs/README.md @@ -5,4 +5,4 @@ available when testing gno code in `_test.gno` and `_filetest.gno` files. Re-declarations of functions already existing override the definitions of the normal stdlibs directory. -Adding imports that don't exist in the corresponding normal stdlib is undefined behavior \ No newline at end of file +Adding imports that don't exist in the corresponding normal stdlib is undefined behavior diff --git a/tm2/Makefile b/tm2/Makefile index 0aaa63e5285..fd3aede0d4c 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -63,3 +63,8 @@ _test.pkg.others:; go test $(GOTEST_FLAGS) `go list ./pkg/... | grep -Ev 'pkg/( _test.pkg.amino:; go test $(GOTEST_FLAGS) ./pkg/amino/... _test.pkg.bft:; go test $(GOTEST_FLAGS) ./pkg/bft/... _test.pkg.db:; go test $(GOTEST_FLAGS) ./pkg/db/... ./pkg/iavl/benchmarks/... + +.PHONY: generate +generate: + go generate -x ./... + $(MAKE) fmt diff --git a/tm2/pkg/overflow/overflow_impl.go b/tm2/pkg/overflow/overflow_impl.go index 0f057f65387..ab9f13c163d 100644 --- a/tm2/pkg/overflow/overflow_impl.go +++ b/tm2/pkg/overflow/overflow_impl.go @@ -84,10 +84,9 @@ func Quotient8(a, b int8) (int8, int8, bool) { } c := a / b status := (c < 0) == ((a < 0) != (b < 0)) || (c == 0) // no sign check for 0 quotient - return c, a%b, status + return c, a % b, status } - // Add16 performs + operation on two int16 operands, returning a result and status. func Add16(a, b int16) (int16, bool) { c := a + b @@ -170,10 +169,9 @@ func Quotient16(a, b int16) (int16, int16, bool) { } c := a / b status := (c < 0) == ((a < 0) != (b < 0)) || (c == 0) // no sign check for 0 quotient - return c, a%b, status + return c, a % b, status } - // Add32 performs + operation on two int32 operands, returning a result and status. func Add32(a, b int32) (int32, bool) { c := a + b @@ -256,10 +254,9 @@ func Quotient32(a, b int32) (int32, int32, bool) { } c := a / b status := (c < 0) == ((a < 0) != (b < 0)) || (c == 0) // no sign check for 0 quotient - return c, a%b, status + return c, a % b, status } - // Add64 performs + operation on two int64 operands, returning a result and status. func Add64(a, b int64) (int64, bool) { c := a + b @@ -342,6 +339,5 @@ func Quotient64(a, b int64) (int64, int64, bool) { } c := a / b status := (c < 0) == ((a < 0) != (b < 0)) || (c == 0) // no sign check for 0 quotient - return c, a%b, status + return c, a % b, status } - From afd39d32dd9adcc5196aa7dfedb628940650c6cd Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 9 Feb 2025 06:39:01 +0100 Subject: [PATCH 47/60] chore: add missing 'make generate' in examples/ folder (#3711) - fixes problems i introduced in #3578. - 15ff464012264733734f93d456169672b8c5ea48 - fixes https://github.com/gnolang/gno/actions/runs/13222603746/job/36909546479?pr=3700. - b9fee1771819f442cfcae17ce5aae4b20a85ef49 - fixes `red` master.] --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .github/workflows/build_template.yml | 14 +++++++++----- examples/Makefile | 5 +++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_template.yml b/.github/workflows/build_template.yml index 454e76b84da..00c14a66cb2 100644 --- a/.github/workflows/build_template.yml +++ b/.github/workflows/build_template.yml @@ -23,10 +23,14 @@ jobs: - name: Check generated files are up to date working-directory: ${{ inputs.modulepath }} run: | - make generate + if make -qp | grep -q '^generate:'; then + make generate - if [ "$(git status -s)" != "" ]; then - echo "command 'go generate' creates file that differ from git tree, please run 'go generate' and commit:" - git status -s - exit 1 + if [ "$(git status -s)" != "" ]; then + echo "command 'make generate' creates files that differ from the git tree, please run 'make generate' and commit:" + git status -s + exit 1 + fi + else + echo "'make generate' rule not found, skipping." fi diff --git a/examples/Makefile b/examples/Makefile index 63a20f78eb9..477fdb6651a 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -63,3 +63,8 @@ fmt: .PHONY: tidy tidy: go run github.com/gnolang/gno/gnovm/cmd/gno mod tidy -v --recursive + +.PHONY: generate +generate: + go generate ./... + $(MAKE) fmt From 262e8ff65b75e0fd82b8939f91c530d1b37753e2 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 9 Feb 2025 12:42:47 +0100 Subject: [PATCH 48/60] fix(examples): hall of fame render order (#3709) ## Description Fixes rendering order in the hall of fame realm. --- examples/gno.land/r/leon/hof/datasource_test.gno | 4 ++-- examples/gno.land/r/leon/hof/render.gno | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/gno.land/r/leon/hof/datasource_test.gno b/examples/gno.land/r/leon/hof/datasource_test.gno index fb67f20e7e7..2cbabb08ddb 100644 --- a/examples/gno.land/r/leon/hof/datasource_test.gno +++ b/examples/gno.land/r/leon/hof/datasource_test.gno @@ -151,7 +151,7 @@ func TestItemRecord(t *testing.T) { content, _ := r.Content() wantContent := "# Submission #1\n\n\n```\ngno.land/r/demo/test\n```\n\nby demo\n\n" + "[View realm](/r/demo/test)\n\nSubmitted at Block #42\n\n" + - "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest) - " + - "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest)\n\n" + "**[2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest) - " + + "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest)**\n\n" uassert.Equal(t, wantContent, content) } diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno index 868262bedc7..cb986e6b42c 100644 --- a/examples/gno.land/r/leon/hof/render.gno +++ b/examples/gno.land/r/leon/hof/render.gno @@ -38,11 +38,9 @@ func (e Exhibition) Render(path string, dashboard bool) string { out += "
\n\n" - page := pager.NewPager(e.itemsSorted, pageSize, false).MustGetPageByPath(path) - - for i := len(page.Items) - 1; i >= 0; i-- { - item := page.Items[i] + page := pager.NewPager(e.itemsSorted, pageSize, true).MustGetPageByPath(path) + for _, item := range page.Items { out += "
\n\n" id, _ := seqid.FromString(item.Key) out += ufmt.Sprintf("### Submission #%d\n\n", int(id)) @@ -63,7 +61,7 @@ func (i Item) Render(dashboard bool) string { out += ufmt.Sprintf("[View realm](%s)\n\n", strings.TrimPrefix(i.pkgpath, "gno.land")) // gno.land/r/leon/home > /r/leon/home out += ufmt.Sprintf("Submitted at Block #%d\n\n", i.blockNum) - out += ufmt.Sprintf("#### [%d👍](%s) - [%d👎](%s)\n\n", + out += ufmt.Sprintf("**[%d👍](%s) - [%d👎](%s)**\n\n", i.upvote.Size(), txlink.Call("Upvote", "pkgpath", i.pkgpath), i.downvote.Size(), txlink.Call("Downvote", "pkgpath", i.pkgpath), ) From 0f5b64de167da304b7e99516db9d3a3da1ab4e73 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 10 Feb 2025 15:41:31 +0100 Subject: [PATCH 49/60] chore: add `gno version` output in `gno bug` body (#3696) --- gnovm/cmd/gno/bug.go | 14 +++++++++----- gnovm/cmd/gno/bug_test.go | 10 +++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/gnovm/cmd/gno/bug.go b/gnovm/cmd/gno/bug.go index 7a4345fb1ed..6f4f78410d6 100644 --- a/gnovm/cmd/gno/bug.go +++ b/gnovm/cmd/gno/bug.go @@ -11,6 +11,7 @@ import ( "text/template" "time" + "github.com/gnolang/gno/gnovm/pkg/version" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -23,9 +24,10 @@ Describe your issue in as much detail as possible here ### Your environment -* Go version: {{.GoVersion}} -* OS and CPU architecture: {{.Os}}/{{.Arch}} -* Gno commit hash causing the issue: {{.Commit}} +* Gno version: {{ .GnoVersion }} +* Go version: {{ .GoVersion }} +* OS and CPU architecture: {{ .Os }}/{{ .Arch }} +* Gno commit hash causing the issue: {{ .Commit }} ### Steps to reproduce @@ -62,10 +64,11 @@ func newBugCmd(io commands.IO) *commands.Command { Name: "bug", ShortUsage: "bug", ShortHelp: "start a bug report", - LongHelp: `opens https://github.com/gnolang/gno/issues in a browser. + LongHelp: `opens https://github.com/gnolang/gno/issues in a browser. The new issue body is prefilled for you with the following information: +- Gno version (the output of "gno version") - Go version (example: go1.22.4) - OS and CPU architecture (example: linux/amd64) - Gno commit hash causing the issue (example: f24690e7ebf325bffcfaf9e328c3df8e6b21e50c) @@ -96,10 +99,11 @@ func execBug(cfg *bugCfg, args []string, io commands.IO) error { } bugReportEnv := struct { - Os, Arch, GoVersion, Commit string + Os, Arch, GnoVersion, GoVersion, Commit string }{ runtime.GOOS, runtime.GOARCH, + version.Version, runtime.Version(), getCommitHash(), } diff --git a/gnovm/cmd/gno/bug_test.go b/gnovm/cmd/gno/bug_test.go index 516bfd4081b..4b0073ce014 100644 --- a/gnovm/cmd/gno/bug_test.go +++ b/gnovm/cmd/gno/bug_test.go @@ -5,17 +5,21 @@ import "testing" func TestBugApp(t *testing.T) { tc := []testMainCase{ { - args: []string{"bug -h"}, - errShouldBe: "flag: help requested", + args: []string{"bug", "-h"}, + errShouldContain: "flag: help requested", }, { - args: []string{"bug unknown"}, + args: []string{"bug", "unknown"}, errShouldBe: "flag: help requested", }, { args: []string{"bug", "-skip-browser"}, stdoutShouldContain: "Go version: go1.", }, + { + args: []string{"bug", "-skip-browser"}, + stdoutShouldContain: "Gno version: develop", + }, } testMainCaseRun(t, tc) } From 03a8b55322e63868bee53be0dd57df3e489128a4 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 10 Feb 2025 16:05:02 +0100 Subject: [PATCH 50/60] build(gnovm): improve code generation (#3595) - follow the standard `^// Code generated .* DO NOT EDIT\.$` syntax in genstd (see `go help generate`) - move `stringer` to `go:generate` directive, output all String methods into one single file --- gnovm/Makefile | 14 +- gnovm/pkg/gnolang/doc.go | 5 + gnovm/pkg/gnolang/kind_string.go | 53 -- gnovm/pkg/gnolang/machine.go | 2 - gnovm/pkg/gnolang/op_string.go | 178 ------- gnovm/pkg/gnolang/string_methods.go | 467 ++++++++++++++++++ gnovm/pkg/gnolang/transctrl_string.go | 26 - gnovm/pkg/gnolang/transfield_string.go | 102 ---- gnovm/pkg/gnolang/vptype_string.go | 48 -- gnovm/pkg/gnolang/word_string.go | 90 ---- gnovm/stdlibs/generated.go | 2 +- gnovm/tests/stdlibs/generated.go | 2 +- misc/genstd/template.tmpl | 2 +- .../testdata/integration/generated.go.golden | 2 +- 14 files changed, 477 insertions(+), 516 deletions(-) delete mode 100644 gnovm/pkg/gnolang/kind_string.go delete mode 100644 gnovm/pkg/gnolang/op_string.go create mode 100644 gnovm/pkg/gnolang/string_methods.go delete mode 100644 gnovm/pkg/gnolang/transctrl_string.go delete mode 100644 gnovm/pkg/gnolang/transfield_string.go delete mode 100644 gnovm/pkg/gnolang/vptype_string.go delete mode 100644 gnovm/pkg/gnolang/word_string.go diff --git a/gnovm/Makefile b/gnovm/Makefile index fa77045d067..ec6c5b06967 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -125,10 +125,8 @@ _test.filetest:; ######################################## # Code gen -# TODO: move _dev.stringer to go:generate instructions, simplify generate -# to just go generate. .PHONY: generate -generate: _dev.stringer _dev.generate _dev.docs fmt imports +generate: _dev.generate _dev.docs fmt imports .PHONY: _dev.docs _dev.docs: @@ -136,16 +134,6 @@ _dev.docs: (go run ./cmd/gno -h 2>&1 || true) | grep -v "exit status 1" > .tmp/gno-help.txt $(rundep) github.com/campoy/embedmd -w `find . -name "*.md"` -stringer_cmd=$(rundep) golang.org/x/tools/cmd/stringer -.PHONY: _dev.stringer -_dev.stringer: - $(stringer_cmd) -type=Kind ./pkg/gnolang - $(stringer_cmd) -type=Op ./pkg/gnolang - $(stringer_cmd) -type=TransCtrl ./pkg/gnolang - $(stringer_cmd) -type=TransField ./pkg/gnolang - $(stringer_cmd) -type=VPType ./pkg/gnolang - $(stringer_cmd) -type=Word ./pkg/gnolang - .PHONY: _dev.generate _dev.generate: go generate -x ./... diff --git a/gnovm/pkg/gnolang/doc.go b/gnovm/pkg/gnolang/doc.go index e52d54470e9..4fe9536d47f 100644 --- a/gnovm/pkg/gnolang/doc.go +++ b/gnovm/pkg/gnolang/doc.go @@ -1,2 +1,7 @@ // SPDX-License-Identifier: GNO License Version 1.0 + +// Package gnolang contains the implementation of the Gno Virtual Machine. package gnolang + +//go:generate -command stringer go run -modfile ../../../misc/devdeps/go.mod golang.org/x/tools/cmd/stringer +//go:generate stringer -type=Kind,Op,TransCtrl,TransField,VPType,Word -output string_methods.go . diff --git a/gnovm/pkg/gnolang/kind_string.go b/gnovm/pkg/gnolang/kind_string.go deleted file mode 100644 index 12e95829b20..00000000000 --- a/gnovm/pkg/gnolang/kind_string.go +++ /dev/null @@ -1,53 +0,0 @@ -// Code generated by "stringer -type=Kind ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[InvalidKind-0] - _ = x[BoolKind-1] - _ = x[StringKind-2] - _ = x[IntKind-3] - _ = x[Int8Kind-4] - _ = x[Int16Kind-5] - _ = x[Int32Kind-6] - _ = x[Int64Kind-7] - _ = x[UintKind-8] - _ = x[Uint8Kind-9] - _ = x[Uint16Kind-10] - _ = x[Uint32Kind-11] - _ = x[Uint64Kind-12] - _ = x[Float32Kind-13] - _ = x[Float64Kind-14] - _ = x[BigintKind-15] - _ = x[BigdecKind-16] - _ = x[ArrayKind-17] - _ = x[SliceKind-18] - _ = x[PointerKind-19] - _ = x[StructKind-20] - _ = x[PackageKind-21] - _ = x[InterfaceKind-22] - _ = x[ChanKind-23] - _ = x[FuncKind-24] - _ = x[MapKind-25] - _ = x[TypeKind-26] - _ = x[BlockKind-27] - _ = x[HeapItemKind-28] - _ = x[TupleKind-29] - _ = x[RefTypeKind-30] -} - -const _Kind_name = "InvalidKindBoolKindStringKindIntKindInt8KindInt16KindInt32KindInt64KindUintKindUint8KindUint16KindUint32KindUint64KindFloat32KindFloat64KindBigintKindBigdecKindArrayKindSliceKindPointerKindStructKindPackageKindInterfaceKindChanKindFuncKindMapKindTypeKindBlockKindHeapItemKindTupleKindRefTypeKind" - -var _Kind_index = [...]uint16{0, 11, 19, 29, 36, 44, 53, 62, 71, 79, 88, 98, 108, 118, 129, 140, 150, 160, 169, 178, 189, 199, 210, 223, 231, 239, 246, 254, 263, 275, 284, 295} - -func (i Kind) String() string { - if i >= Kind(len(_Kind_index)-1) { - return "Kind(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Kind_name[_Kind_index[i]:_Kind_index[i+1]] -} diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index f7d2cf10f2c..8a640f21072 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -1,7 +1,5 @@ package gnolang -// XXX rename file to machine.go. - import ( "fmt" "io" diff --git a/gnovm/pkg/gnolang/op_string.go b/gnovm/pkg/gnolang/op_string.go deleted file mode 100644 index b13bb8f278e..00000000000 --- a/gnovm/pkg/gnolang/op_string.go +++ /dev/null @@ -1,178 +0,0 @@ -// Code generated by "stringer -type=Op ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[OpInvalid-0] - _ = x[OpHalt-1] - _ = x[OpNoop-2] - _ = x[OpExec-3] - _ = x[OpPrecall-4] - _ = x[OpCall-5] - _ = x[OpCallNativeBody-6] - _ = x[OpReturn-7] - _ = x[OpReturnFromBlock-8] - _ = x[OpReturnToBlock-9] - _ = x[OpDefer-10] - _ = x[OpCallDeferNativeBody-11] - _ = x[OpGo-12] - _ = x[OpSelect-13] - _ = x[OpSwitchClause-14] - _ = x[OpSwitchClauseCase-15] - _ = x[OpTypeSwitch-16] - _ = x[OpIfCond-17] - _ = x[OpPopValue-18] - _ = x[OpPopResults-19] - _ = x[OpPopBlock-20] - _ = x[OpPopFrameAndReset-21] - _ = x[OpPanic1-22] - _ = x[OpPanic2-23] - _ = x[OpUpos-32] - _ = x[OpUneg-33] - _ = x[OpUnot-34] - _ = x[OpUxor-35] - _ = x[OpUrecv-37] - _ = x[OpLor-38] - _ = x[OpLand-39] - _ = x[OpEql-40] - _ = x[OpNeq-41] - _ = x[OpLss-42] - _ = x[OpLeq-43] - _ = x[OpGtr-44] - _ = x[OpGeq-45] - _ = x[OpAdd-46] - _ = x[OpSub-47] - _ = x[OpBor-48] - _ = x[OpXor-49] - _ = x[OpMul-50] - _ = x[OpQuo-51] - _ = x[OpRem-52] - _ = x[OpShl-53] - _ = x[OpShr-54] - _ = x[OpBand-55] - _ = x[OpBandn-56] - _ = x[OpEval-64] - _ = x[OpBinary1-65] - _ = x[OpIndex1-66] - _ = x[OpIndex2-67] - _ = x[OpSelector-68] - _ = x[OpSlice-69] - _ = x[OpStar-70] - _ = x[OpRef-71] - _ = x[OpTypeAssert1-72] - _ = x[OpTypeAssert2-73] - _ = x[OpStaticTypeOf-74] - _ = x[OpCompositeLit-75] - _ = x[OpArrayLit-76] - _ = x[OpSliceLit-77] - _ = x[OpSliceLit2-78] - _ = x[OpMapLit-79] - _ = x[OpStructLit-80] - _ = x[OpFuncLit-81] - _ = x[OpConvert-82] - _ = x[OpArrayLitGoNative-96] - _ = x[OpSliceLitGoNative-97] - _ = x[OpStructLitGoNative-98] - _ = x[OpCallGoNative-99] - _ = x[OpFieldType-112] - _ = x[OpArrayType-113] - _ = x[OpSliceType-114] - _ = x[OpPointerType-115] - _ = x[OpInterfaceType-116] - _ = x[OpChanType-117] - _ = x[OpFuncType-118] - _ = x[OpMapType-119] - _ = x[OpStructType-120] - _ = x[OpMaybeNativeType-121] - _ = x[OpAssign-128] - _ = x[OpAddAssign-129] - _ = x[OpSubAssign-130] - _ = x[OpMulAssign-131] - _ = x[OpQuoAssign-132] - _ = x[OpRemAssign-133] - _ = x[OpBandAssign-134] - _ = x[OpBandnAssign-135] - _ = x[OpBorAssign-136] - _ = x[OpXorAssign-137] - _ = x[OpShlAssign-138] - _ = x[OpShrAssign-139] - _ = x[OpDefine-140] - _ = x[OpInc-141] - _ = x[OpDec-142] - _ = x[OpValueDecl-144] - _ = x[OpTypeDecl-145] - _ = x[OpSticky-208] - _ = x[OpBody-209] - _ = x[OpForLoop-210] - _ = x[OpRangeIter-211] - _ = x[OpRangeIterString-212] - _ = x[OpRangeIterMap-213] - _ = x[OpRangeIterArrayPtr-214] - _ = x[OpReturnCallDefers-215] - _ = x[OpVoid-255] -} - -const ( - _Op_name_0 = "OpInvalidOpHaltOpNoopOpExecOpPrecallOpCallOpCallNativeBodyOpReturnOpReturnFromBlockOpReturnToBlockOpDeferOpCallDeferNativeBodyOpGoOpSelectOpSwitchClauseOpSwitchClauseCaseOpTypeSwitchOpIfCondOpPopValueOpPopResultsOpPopBlockOpPopFrameAndResetOpPanic1OpPanic2" - _Op_name_1 = "OpUposOpUnegOpUnotOpUxor" - _Op_name_2 = "OpUrecvOpLorOpLandOpEqlOpNeqOpLssOpLeqOpGtrOpGeqOpAddOpSubOpBorOpXorOpMulOpQuoOpRemOpShlOpShrOpBandOpBandn" - _Op_name_3 = "OpEvalOpBinary1OpIndex1OpIndex2OpSelectorOpSliceOpStarOpRefOpTypeAssert1OpTypeAssert2OpStaticTypeOfOpCompositeLitOpArrayLitOpSliceLitOpSliceLit2OpMapLitOpStructLitOpFuncLitOpConvert" - _Op_name_4 = "OpArrayLitGoNativeOpSliceLitGoNativeOpStructLitGoNativeOpCallGoNative" - _Op_name_5 = "OpFieldTypeOpArrayTypeOpSliceTypeOpPointerTypeOpInterfaceTypeOpChanTypeOpFuncTypeOpMapTypeOpStructTypeOpMaybeNativeType" - _Op_name_6 = "OpAssignOpAddAssignOpSubAssignOpMulAssignOpQuoAssignOpRemAssignOpBandAssignOpBandnAssignOpBorAssignOpXorAssignOpShlAssignOpShrAssignOpDefineOpIncOpDec" - _Op_name_7 = "OpValueDeclOpTypeDecl" - _Op_name_8 = "OpStickyOpBodyOpForLoopOpRangeIterOpRangeIterStringOpRangeIterMapOpRangeIterArrayPtrOpReturnCallDefers" - _Op_name_9 = "OpVoid" -) - -var ( - _Op_index_0 = [...]uint16{0, 9, 15, 21, 27, 36, 42, 58, 66, 83, 98, 105, 126, 130, 138, 152, 170, 182, 190, 200, 212, 222, 240, 248, 256} - _Op_index_1 = [...]uint8{0, 6, 12, 18, 24} - _Op_index_2 = [...]uint8{0, 7, 12, 18, 23, 28, 33, 38, 43, 48, 53, 58, 63, 68, 73, 78, 83, 88, 93, 99, 106} - _Op_index_3 = [...]uint8{0, 6, 15, 23, 31, 41, 48, 54, 59, 72, 85, 99, 113, 123, 133, 144, 152, 163, 172, 181} - _Op_index_4 = [...]uint8{0, 18, 36, 55, 69} - _Op_index_5 = [...]uint8{0, 11, 22, 33, 46, 61, 71, 81, 90, 102, 119} - _Op_index_6 = [...]uint8{0, 8, 19, 30, 41, 52, 63, 75, 88, 99, 110, 121, 132, 140, 145, 150} - _Op_index_7 = [...]uint8{0, 11, 21} - _Op_index_8 = [...]uint8{0, 8, 14, 23, 34, 51, 65, 84, 102} -) - -func (i Op) String() string { - switch { - case i <= 23: - return _Op_name_0[_Op_index_0[i]:_Op_index_0[i+1]] - case 32 <= i && i <= 35: - i -= 32 - return _Op_name_1[_Op_index_1[i]:_Op_index_1[i+1]] - case 37 <= i && i <= 56: - i -= 37 - return _Op_name_2[_Op_index_2[i]:_Op_index_2[i+1]] - case 64 <= i && i <= 82: - i -= 64 - return _Op_name_3[_Op_index_3[i]:_Op_index_3[i+1]] - case 96 <= i && i <= 99: - i -= 96 - return _Op_name_4[_Op_index_4[i]:_Op_index_4[i+1]] - case 112 <= i && i <= 121: - i -= 112 - return _Op_name_5[_Op_index_5[i]:_Op_index_5[i+1]] - case 128 <= i && i <= 142: - i -= 128 - return _Op_name_6[_Op_index_6[i]:_Op_index_6[i+1]] - case 144 <= i && i <= 145: - i -= 144 - return _Op_name_7[_Op_index_7[i]:_Op_index_7[i+1]] - case 208 <= i && i <= 215: - i -= 208 - return _Op_name_8[_Op_index_8[i]:_Op_index_8[i+1]] - case i == 255: - return _Op_name_9 - default: - return "Op(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/gnovm/pkg/gnolang/string_methods.go b/gnovm/pkg/gnolang/string_methods.go new file mode 100644 index 00000000000..460e308fa9b --- /dev/null +++ b/gnovm/pkg/gnolang/string_methods.go @@ -0,0 +1,467 @@ +// Code generated by "stringer -type=Kind,Op,TransCtrl,TransField,VPType,Word -output string_methods.go ."; DO NOT EDIT. + +package gnolang + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidKind-0] + _ = x[BoolKind-1] + _ = x[StringKind-2] + _ = x[IntKind-3] + _ = x[Int8Kind-4] + _ = x[Int16Kind-5] + _ = x[Int32Kind-6] + _ = x[Int64Kind-7] + _ = x[UintKind-8] + _ = x[Uint8Kind-9] + _ = x[Uint16Kind-10] + _ = x[Uint32Kind-11] + _ = x[Uint64Kind-12] + _ = x[Float32Kind-13] + _ = x[Float64Kind-14] + _ = x[BigintKind-15] + _ = x[BigdecKind-16] + _ = x[ArrayKind-17] + _ = x[SliceKind-18] + _ = x[PointerKind-19] + _ = x[StructKind-20] + _ = x[PackageKind-21] + _ = x[InterfaceKind-22] + _ = x[ChanKind-23] + _ = x[FuncKind-24] + _ = x[MapKind-25] + _ = x[TypeKind-26] + _ = x[BlockKind-27] + _ = x[HeapItemKind-28] + _ = x[TupleKind-29] + _ = x[RefTypeKind-30] +} + +const _Kind_name = "InvalidKindBoolKindStringKindIntKindInt8KindInt16KindInt32KindInt64KindUintKindUint8KindUint16KindUint32KindUint64KindFloat32KindFloat64KindBigintKindBigdecKindArrayKindSliceKindPointerKindStructKindPackageKindInterfaceKindChanKindFuncKindMapKindTypeKindBlockKindHeapItemKindTupleKindRefTypeKind" + +var _Kind_index = [...]uint16{0, 11, 19, 29, 36, 44, 53, 62, 71, 79, 88, 98, 108, 118, 129, 140, 150, 160, 169, 178, 189, 199, 210, 223, 231, 239, 246, 254, 263, 275, 284, 295} + +func (i Kind) String() string { + if i >= Kind(len(_Kind_index)-1) { + return "Kind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Kind_name[_Kind_index[i]:_Kind_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[OpInvalid-0] + _ = x[OpHalt-1] + _ = x[OpNoop-2] + _ = x[OpExec-3] + _ = x[OpPrecall-4] + _ = x[OpCall-5] + _ = x[OpCallNativeBody-6] + _ = x[OpReturn-7] + _ = x[OpReturnFromBlock-8] + _ = x[OpReturnToBlock-9] + _ = x[OpDefer-10] + _ = x[OpCallDeferNativeBody-11] + _ = x[OpGo-12] + _ = x[OpSelect-13] + _ = x[OpSwitchClause-14] + _ = x[OpSwitchClauseCase-15] + _ = x[OpTypeSwitch-16] + _ = x[OpIfCond-17] + _ = x[OpPopValue-18] + _ = x[OpPopResults-19] + _ = x[OpPopBlock-20] + _ = x[OpPopFrameAndReset-21] + _ = x[OpPanic1-22] + _ = x[OpPanic2-23] + _ = x[OpUpos-32] + _ = x[OpUneg-33] + _ = x[OpUnot-34] + _ = x[OpUxor-35] + _ = x[OpUrecv-37] + _ = x[OpLor-38] + _ = x[OpLand-39] + _ = x[OpEql-40] + _ = x[OpNeq-41] + _ = x[OpLss-42] + _ = x[OpLeq-43] + _ = x[OpGtr-44] + _ = x[OpGeq-45] + _ = x[OpAdd-46] + _ = x[OpSub-47] + _ = x[OpBor-48] + _ = x[OpXor-49] + _ = x[OpMul-50] + _ = x[OpQuo-51] + _ = x[OpRem-52] + _ = x[OpShl-53] + _ = x[OpShr-54] + _ = x[OpBand-55] + _ = x[OpBandn-56] + _ = x[OpEval-64] + _ = x[OpBinary1-65] + _ = x[OpIndex1-66] + _ = x[OpIndex2-67] + _ = x[OpSelector-68] + _ = x[OpSlice-69] + _ = x[OpStar-70] + _ = x[OpRef-71] + _ = x[OpTypeAssert1-72] + _ = x[OpTypeAssert2-73] + _ = x[OpStaticTypeOf-74] + _ = x[OpCompositeLit-75] + _ = x[OpArrayLit-76] + _ = x[OpSliceLit-77] + _ = x[OpSliceLit2-78] + _ = x[OpMapLit-79] + _ = x[OpStructLit-80] + _ = x[OpFuncLit-81] + _ = x[OpConvert-82] + _ = x[OpArrayLitGoNative-96] + _ = x[OpSliceLitGoNative-97] + _ = x[OpStructLitGoNative-98] + _ = x[OpCallGoNative-99] + _ = x[OpFieldType-112] + _ = x[OpArrayType-113] + _ = x[OpSliceType-114] + _ = x[OpPointerType-115] + _ = x[OpInterfaceType-116] + _ = x[OpChanType-117] + _ = x[OpFuncType-118] + _ = x[OpMapType-119] + _ = x[OpStructType-120] + _ = x[OpMaybeNativeType-121] + _ = x[OpAssign-128] + _ = x[OpAddAssign-129] + _ = x[OpSubAssign-130] + _ = x[OpMulAssign-131] + _ = x[OpQuoAssign-132] + _ = x[OpRemAssign-133] + _ = x[OpBandAssign-134] + _ = x[OpBandnAssign-135] + _ = x[OpBorAssign-136] + _ = x[OpXorAssign-137] + _ = x[OpShlAssign-138] + _ = x[OpShrAssign-139] + _ = x[OpDefine-140] + _ = x[OpInc-141] + _ = x[OpDec-142] + _ = x[OpValueDecl-144] + _ = x[OpTypeDecl-145] + _ = x[OpSticky-208] + _ = x[OpBody-209] + _ = x[OpForLoop-210] + _ = x[OpRangeIter-211] + _ = x[OpRangeIterString-212] + _ = x[OpRangeIterMap-213] + _ = x[OpRangeIterArrayPtr-214] + _ = x[OpReturnCallDefers-215] + _ = x[OpVoid-255] +} + +const ( + _Op_name_0 = "OpInvalidOpHaltOpNoopOpExecOpPrecallOpCallOpCallNativeBodyOpReturnOpReturnFromBlockOpReturnToBlockOpDeferOpCallDeferNativeBodyOpGoOpSelectOpSwitchClauseOpSwitchClauseCaseOpTypeSwitchOpIfCondOpPopValueOpPopResultsOpPopBlockOpPopFrameAndResetOpPanic1OpPanic2" + _Op_name_1 = "OpUposOpUnegOpUnotOpUxor" + _Op_name_2 = "OpUrecvOpLorOpLandOpEqlOpNeqOpLssOpLeqOpGtrOpGeqOpAddOpSubOpBorOpXorOpMulOpQuoOpRemOpShlOpShrOpBandOpBandn" + _Op_name_3 = "OpEvalOpBinary1OpIndex1OpIndex2OpSelectorOpSliceOpStarOpRefOpTypeAssert1OpTypeAssert2OpStaticTypeOfOpCompositeLitOpArrayLitOpSliceLitOpSliceLit2OpMapLitOpStructLitOpFuncLitOpConvert" + _Op_name_4 = "OpArrayLitGoNativeOpSliceLitGoNativeOpStructLitGoNativeOpCallGoNative" + _Op_name_5 = "OpFieldTypeOpArrayTypeOpSliceTypeOpPointerTypeOpInterfaceTypeOpChanTypeOpFuncTypeOpMapTypeOpStructTypeOpMaybeNativeType" + _Op_name_6 = "OpAssignOpAddAssignOpSubAssignOpMulAssignOpQuoAssignOpRemAssignOpBandAssignOpBandnAssignOpBorAssignOpXorAssignOpShlAssignOpShrAssignOpDefineOpIncOpDec" + _Op_name_7 = "OpValueDeclOpTypeDecl" + _Op_name_8 = "OpStickyOpBodyOpForLoopOpRangeIterOpRangeIterStringOpRangeIterMapOpRangeIterArrayPtrOpReturnCallDefers" + _Op_name_9 = "OpVoid" +) + +var ( + _Op_index_0 = [...]uint16{0, 9, 15, 21, 27, 36, 42, 58, 66, 83, 98, 105, 126, 130, 138, 152, 170, 182, 190, 200, 212, 222, 240, 248, 256} + _Op_index_1 = [...]uint8{0, 6, 12, 18, 24} + _Op_index_2 = [...]uint8{0, 7, 12, 18, 23, 28, 33, 38, 43, 48, 53, 58, 63, 68, 73, 78, 83, 88, 93, 99, 106} + _Op_index_3 = [...]uint8{0, 6, 15, 23, 31, 41, 48, 54, 59, 72, 85, 99, 113, 123, 133, 144, 152, 163, 172, 181} + _Op_index_4 = [...]uint8{0, 18, 36, 55, 69} + _Op_index_5 = [...]uint8{0, 11, 22, 33, 46, 61, 71, 81, 90, 102, 119} + _Op_index_6 = [...]uint8{0, 8, 19, 30, 41, 52, 63, 75, 88, 99, 110, 121, 132, 140, 145, 150} + _Op_index_7 = [...]uint8{0, 11, 21} + _Op_index_8 = [...]uint8{0, 8, 14, 23, 34, 51, 65, 84, 102} +) + +func (i Op) String() string { + switch { + case i <= 23: + return _Op_name_0[_Op_index_0[i]:_Op_index_0[i+1]] + case 32 <= i && i <= 35: + i -= 32 + return _Op_name_1[_Op_index_1[i]:_Op_index_1[i+1]] + case 37 <= i && i <= 56: + i -= 37 + return _Op_name_2[_Op_index_2[i]:_Op_index_2[i+1]] + case 64 <= i && i <= 82: + i -= 64 + return _Op_name_3[_Op_index_3[i]:_Op_index_3[i+1]] + case 96 <= i && i <= 99: + i -= 96 + return _Op_name_4[_Op_index_4[i]:_Op_index_4[i+1]] + case 112 <= i && i <= 121: + i -= 112 + return _Op_name_5[_Op_index_5[i]:_Op_index_5[i+1]] + case 128 <= i && i <= 142: + i -= 128 + return _Op_name_6[_Op_index_6[i]:_Op_index_6[i+1]] + case 144 <= i && i <= 145: + i -= 144 + return _Op_name_7[_Op_index_7[i]:_Op_index_7[i+1]] + case 208 <= i && i <= 215: + i -= 208 + return _Op_name_8[_Op_index_8[i]:_Op_index_8[i+1]] + case i == 255: + return _Op_name_9 + default: + return "Op(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TRANS_CONTINUE-0] + _ = x[TRANS_SKIP-1] + _ = x[TRANS_BREAK-2] + _ = x[TRANS_EXIT-3] +} + +const _TransCtrl_name = "TRANS_CONTINUETRANS_SKIPTRANS_BREAKTRANS_EXIT" + +var _TransCtrl_index = [...]uint8{0, 14, 24, 35, 45} + +func (i TransCtrl) String() string { + if i >= TransCtrl(len(_TransCtrl_index)-1) { + return "TransCtrl(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _TransCtrl_name[_TransCtrl_index[i]:_TransCtrl_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TRANS_ROOT-0] + _ = x[TRANS_BINARY_LEFT-1] + _ = x[TRANS_BINARY_RIGHT-2] + _ = x[TRANS_CALL_FUNC-3] + _ = x[TRANS_CALL_ARG-4] + _ = x[TRANS_INDEX_X-5] + _ = x[TRANS_INDEX_INDEX-6] + _ = x[TRANS_SELECTOR_X-7] + _ = x[TRANS_SLICE_X-8] + _ = x[TRANS_SLICE_LOW-9] + _ = x[TRANS_SLICE_HIGH-10] + _ = x[TRANS_SLICE_MAX-11] + _ = x[TRANS_STAR_X-12] + _ = x[TRANS_REF_X-13] + _ = x[TRANS_TYPEASSERT_X-14] + _ = x[TRANS_TYPEASSERT_TYPE-15] + _ = x[TRANS_UNARY_X-16] + _ = x[TRANS_COMPOSITE_TYPE-17] + _ = x[TRANS_COMPOSITE_KEY-18] + _ = x[TRANS_COMPOSITE_VALUE-19] + _ = x[TRANS_FUNCLIT_TYPE-20] + _ = x[TRANS_FUNCLIT_HEAP_CAPTURE-21] + _ = x[TRANS_FUNCLIT_BODY-22] + _ = x[TRANS_FIELDTYPE_TYPE-23] + _ = x[TRANS_FIELDTYPE_TAG-24] + _ = x[TRANS_ARRAYTYPE_LEN-25] + _ = x[TRANS_ARRAYTYPE_ELT-26] + _ = x[TRANS_SLICETYPE_ELT-27] + _ = x[TRANS_INTERFACETYPE_METHOD-28] + _ = x[TRANS_CHANTYPE_VALUE-29] + _ = x[TRANS_FUNCTYPE_PARAM-30] + _ = x[TRANS_FUNCTYPE_RESULT-31] + _ = x[TRANS_MAPTYPE_KEY-32] + _ = x[TRANS_MAPTYPE_VALUE-33] + _ = x[TRANS_STRUCTTYPE_FIELD-34] + _ = x[TRANS_MAYBENATIVETYPE_TYPE-35] + _ = x[TRANS_ASSIGN_LHS-36] + _ = x[TRANS_ASSIGN_RHS-37] + _ = x[TRANS_BLOCK_BODY-38] + _ = x[TRANS_DECL_BODY-39] + _ = x[TRANS_DEFER_CALL-40] + _ = x[TRANS_EXPR_X-41] + _ = x[TRANS_FOR_INIT-42] + _ = x[TRANS_FOR_COND-43] + _ = x[TRANS_FOR_POST-44] + _ = x[TRANS_FOR_BODY-45] + _ = x[TRANS_GO_CALL-46] + _ = x[TRANS_IF_INIT-47] + _ = x[TRANS_IF_COND-48] + _ = x[TRANS_IF_BODY-49] + _ = x[TRANS_IF_ELSE-50] + _ = x[TRANS_IF_CASE_BODY-51] + _ = x[TRANS_INCDEC_X-52] + _ = x[TRANS_RANGE_X-53] + _ = x[TRANS_RANGE_KEY-54] + _ = x[TRANS_RANGE_VALUE-55] + _ = x[TRANS_RANGE_BODY-56] + _ = x[TRANS_RETURN_RESULT-57] + _ = x[TRANS_PANIC_EXCEPTION-58] + _ = x[TRANS_SELECT_CASE-59] + _ = x[TRANS_SELECTCASE_COMM-60] + _ = x[TRANS_SELECTCASE_BODY-61] + _ = x[TRANS_SEND_CHAN-62] + _ = x[TRANS_SEND_VALUE-63] + _ = x[TRANS_SWITCH_INIT-64] + _ = x[TRANS_SWITCH_X-65] + _ = x[TRANS_SWITCH_CASE-66] + _ = x[TRANS_SWITCHCASE_CASE-67] + _ = x[TRANS_SWITCHCASE_BODY-68] + _ = x[TRANS_FUNC_RECV-69] + _ = x[TRANS_FUNC_TYPE-70] + _ = x[TRANS_FUNC_BODY-71] + _ = x[TRANS_IMPORT_PATH-72] + _ = x[TRANS_CONST_TYPE-73] + _ = x[TRANS_CONST_VALUE-74] + _ = x[TRANS_VAR_NAME-75] + _ = x[TRANS_VAR_TYPE-76] + _ = x[TRANS_VAR_VALUE-77] + _ = x[TRANS_TYPE_TYPE-78] + _ = x[TRANS_FILE_BODY-79] +} + +const _TransField_name = "TRANS_ROOTTRANS_BINARY_LEFTTRANS_BINARY_RIGHTTRANS_CALL_FUNCTRANS_CALL_ARGTRANS_INDEX_XTRANS_INDEX_INDEXTRANS_SELECTOR_XTRANS_SLICE_XTRANS_SLICE_LOWTRANS_SLICE_HIGHTRANS_SLICE_MAXTRANS_STAR_XTRANS_REF_XTRANS_TYPEASSERT_XTRANS_TYPEASSERT_TYPETRANS_UNARY_XTRANS_COMPOSITE_TYPETRANS_COMPOSITE_KEYTRANS_COMPOSITE_VALUETRANS_FUNCLIT_TYPETRANS_FUNCLIT_HEAP_CAPTURETRANS_FUNCLIT_BODYTRANS_FIELDTYPE_TYPETRANS_FIELDTYPE_TAGTRANS_ARRAYTYPE_LENTRANS_ARRAYTYPE_ELTTRANS_SLICETYPE_ELTTRANS_INTERFACETYPE_METHODTRANS_CHANTYPE_VALUETRANS_FUNCTYPE_PARAMTRANS_FUNCTYPE_RESULTTRANS_MAPTYPE_KEYTRANS_MAPTYPE_VALUETRANS_STRUCTTYPE_FIELDTRANS_MAYBENATIVETYPE_TYPETRANS_ASSIGN_LHSTRANS_ASSIGN_RHSTRANS_BLOCK_BODYTRANS_DECL_BODYTRANS_DEFER_CALLTRANS_EXPR_XTRANS_FOR_INITTRANS_FOR_CONDTRANS_FOR_POSTTRANS_FOR_BODYTRANS_GO_CALLTRANS_IF_INITTRANS_IF_CONDTRANS_IF_BODYTRANS_IF_ELSETRANS_IF_CASE_BODYTRANS_INCDEC_XTRANS_RANGE_XTRANS_RANGE_KEYTRANS_RANGE_VALUETRANS_RANGE_BODYTRANS_RETURN_RESULTTRANS_PANIC_EXCEPTIONTRANS_SELECT_CASETRANS_SELECTCASE_COMMTRANS_SELECTCASE_BODYTRANS_SEND_CHANTRANS_SEND_VALUETRANS_SWITCH_INITTRANS_SWITCH_XTRANS_SWITCH_CASETRANS_SWITCHCASE_CASETRANS_SWITCHCASE_BODYTRANS_FUNC_RECVTRANS_FUNC_TYPETRANS_FUNC_BODYTRANS_IMPORT_PATHTRANS_CONST_TYPETRANS_CONST_VALUETRANS_VAR_NAMETRANS_VAR_TYPETRANS_VAR_VALUETRANS_TYPE_TYPETRANS_FILE_BODY" + +var _TransField_index = [...]uint16{0, 10, 27, 45, 60, 74, 87, 104, 120, 133, 148, 164, 179, 191, 202, 220, 241, 254, 274, 293, 314, 332, 358, 376, 396, 415, 434, 453, 472, 498, 518, 538, 559, 576, 595, 617, 643, 659, 675, 691, 706, 722, 734, 748, 762, 776, 790, 803, 816, 829, 842, 855, 873, 887, 900, 915, 932, 948, 967, 988, 1005, 1026, 1047, 1062, 1078, 1095, 1109, 1126, 1147, 1168, 1183, 1198, 1213, 1230, 1246, 1263, 1277, 1291, 1306, 1321, 1336} + +func (i TransField) String() string { + if i >= TransField(len(_TransField_index)-1) { + return "TransField(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _TransField_name[_TransField_index[i]:_TransField_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[VPUverse-0] + _ = x[VPBlock-1] + _ = x[VPField-2] + _ = x[VPValMethod-3] + _ = x[VPPtrMethod-4] + _ = x[VPInterface-5] + _ = x[VPSubrefField-6] + _ = x[VPDerefField-18] + _ = x[VPDerefValMethod-19] + _ = x[VPDerefPtrMethod-20] + _ = x[VPDerefInterface-21] + _ = x[VPNative-32] +} + +const ( + _VPType_name_0 = "VPUverseVPBlockVPFieldVPValMethodVPPtrMethodVPInterfaceVPSubrefField" + _VPType_name_1 = "VPDerefFieldVPDerefValMethodVPDerefPtrMethodVPDerefInterface" + _VPType_name_2 = "VPNative" +) + +var ( + _VPType_index_0 = [...]uint8{0, 8, 15, 22, 33, 44, 55, 68} + _VPType_index_1 = [...]uint8{0, 12, 28, 44, 60} +) + +func (i VPType) String() string { + switch { + case i <= 6: + return _VPType_name_0[_VPType_index_0[i]:_VPType_index_0[i+1]] + case 18 <= i && i <= 21: + i -= 18 + return _VPType_name_1[_VPType_index_1[i]:_VPType_index_1[i+1]] + case i == 32: + return _VPType_name_2 + default: + return "VPType(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ILLEGAL-0] + _ = x[NAME-1] + _ = x[INT-2] + _ = x[FLOAT-3] + _ = x[IMAG-4] + _ = x[CHAR-5] + _ = x[STRING-6] + _ = x[ADD-7] + _ = x[SUB-8] + _ = x[MUL-9] + _ = x[QUO-10] + _ = x[REM-11] + _ = x[BAND-12] + _ = x[BOR-13] + _ = x[XOR-14] + _ = x[SHL-15] + _ = x[SHR-16] + _ = x[BAND_NOT-17] + _ = x[ADD_ASSIGN-18] + _ = x[SUB_ASSIGN-19] + _ = x[MUL_ASSIGN-20] + _ = x[QUO_ASSIGN-21] + _ = x[REM_ASSIGN-22] + _ = x[BAND_ASSIGN-23] + _ = x[BOR_ASSIGN-24] + _ = x[XOR_ASSIGN-25] + _ = x[SHL_ASSIGN-26] + _ = x[SHR_ASSIGN-27] + _ = x[BAND_NOT_ASSIGN-28] + _ = x[LAND-29] + _ = x[LOR-30] + _ = x[ARROW-31] + _ = x[INC-32] + _ = x[DEC-33] + _ = x[EQL-34] + _ = x[LSS-35] + _ = x[GTR-36] + _ = x[ASSIGN-37] + _ = x[NOT-38] + _ = x[NEQ-39] + _ = x[LEQ-40] + _ = x[GEQ-41] + _ = x[DEFINE-42] + _ = x[BREAK-43] + _ = x[CASE-44] + _ = x[CHAN-45] + _ = x[CONST-46] + _ = x[CONTINUE-47] + _ = x[DEFAULT-48] + _ = x[DEFER-49] + _ = x[ELSE-50] + _ = x[FALLTHROUGH-51] + _ = x[FOR-52] + _ = x[FUNC-53] + _ = x[GO-54] + _ = x[GOTO-55] + _ = x[IF-56] + _ = x[IMPORT-57] + _ = x[INTERFACE-58] + _ = x[MAP-59] + _ = x[PACKAGE-60] + _ = x[RANGE-61] + _ = x[RETURN-62] + _ = x[SELECT-63] + _ = x[STRUCT-64] + _ = x[SWITCH-65] + _ = x[TYPE-66] + _ = x[VAR-67] +} + +const _Word_name = "ILLEGALNAMEINTFLOATIMAGCHARSTRINGADDSUBMULQUOREMBANDBORXORSHLSHRBAND_NOTADD_ASSIGNSUB_ASSIGNMUL_ASSIGNQUO_ASSIGNREM_ASSIGNBAND_ASSIGNBOR_ASSIGNXOR_ASSIGNSHL_ASSIGNSHR_ASSIGNBAND_NOT_ASSIGNLANDLORARROWINCDECEQLLSSGTRASSIGNNOTNEQLEQGEQDEFINEBREAKCASECHANCONSTCONTINUEDEFAULTDEFERELSEFALLTHROUGHFORFUNCGOGOTOIFIMPORTINTERFACEMAPPACKAGERANGERETURNSELECTSTRUCTSWITCHTYPEVAR" + +var _Word_index = [...]uint16{0, 7, 11, 14, 19, 23, 27, 33, 36, 39, 42, 45, 48, 52, 55, 58, 61, 64, 72, 82, 92, 102, 112, 122, 133, 143, 153, 163, 173, 188, 192, 195, 200, 203, 206, 209, 212, 215, 221, 224, 227, 230, 233, 239, 244, 248, 252, 257, 265, 272, 277, 281, 292, 295, 299, 301, 305, 307, 313, 322, 325, 332, 337, 343, 349, 355, 361, 365, 368} + +func (i Word) String() string { + if i < 0 || i >= Word(len(_Word_index)-1) { + return "Word(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Word_name[_Word_index[i]:_Word_index[i+1]] +} diff --git a/gnovm/pkg/gnolang/transctrl_string.go b/gnovm/pkg/gnolang/transctrl_string.go deleted file mode 100644 index 92d33c65da5..00000000000 --- a/gnovm/pkg/gnolang/transctrl_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=TransCtrl ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[TRANS_CONTINUE-0] - _ = x[TRANS_SKIP-1] - _ = x[TRANS_BREAK-2] - _ = x[TRANS_EXIT-3] -} - -const _TransCtrl_name = "TRANS_CONTINUETRANS_SKIPTRANS_BREAKTRANS_EXIT" - -var _TransCtrl_index = [...]uint8{0, 14, 24, 35, 45} - -func (i TransCtrl) String() string { - if i >= TransCtrl(len(_TransCtrl_index)-1) { - return "TransCtrl(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _TransCtrl_name[_TransCtrl_index[i]:_TransCtrl_index[i+1]] -} diff --git a/gnovm/pkg/gnolang/transfield_string.go b/gnovm/pkg/gnolang/transfield_string.go deleted file mode 100644 index 31afcf2be0d..00000000000 --- a/gnovm/pkg/gnolang/transfield_string.go +++ /dev/null @@ -1,102 +0,0 @@ -// Code generated by "stringer -type=TransField ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[TRANS_ROOT-0] - _ = x[TRANS_BINARY_LEFT-1] - _ = x[TRANS_BINARY_RIGHT-2] - _ = x[TRANS_CALL_FUNC-3] - _ = x[TRANS_CALL_ARG-4] - _ = x[TRANS_INDEX_X-5] - _ = x[TRANS_INDEX_INDEX-6] - _ = x[TRANS_SELECTOR_X-7] - _ = x[TRANS_SLICE_X-8] - _ = x[TRANS_SLICE_LOW-9] - _ = x[TRANS_SLICE_HIGH-10] - _ = x[TRANS_SLICE_MAX-11] - _ = x[TRANS_STAR_X-12] - _ = x[TRANS_REF_X-13] - _ = x[TRANS_TYPEASSERT_X-14] - _ = x[TRANS_TYPEASSERT_TYPE-15] - _ = x[TRANS_UNARY_X-16] - _ = x[TRANS_COMPOSITE_TYPE-17] - _ = x[TRANS_COMPOSITE_KEY-18] - _ = x[TRANS_COMPOSITE_VALUE-19] - _ = x[TRANS_FUNCLIT_TYPE-20] - _ = x[TRANS_FUNCLIT_HEAP_CAPTURE-21] - _ = x[TRANS_FUNCLIT_BODY-22] - _ = x[TRANS_FIELDTYPE_TYPE-23] - _ = x[TRANS_FIELDTYPE_TAG-24] - _ = x[TRANS_ARRAYTYPE_LEN-25] - _ = x[TRANS_ARRAYTYPE_ELT-26] - _ = x[TRANS_SLICETYPE_ELT-27] - _ = x[TRANS_INTERFACETYPE_METHOD-28] - _ = x[TRANS_CHANTYPE_VALUE-29] - _ = x[TRANS_FUNCTYPE_PARAM-30] - _ = x[TRANS_FUNCTYPE_RESULT-31] - _ = x[TRANS_MAPTYPE_KEY-32] - _ = x[TRANS_MAPTYPE_VALUE-33] - _ = x[TRANS_STRUCTTYPE_FIELD-34] - _ = x[TRANS_MAYBENATIVETYPE_TYPE-35] - _ = x[TRANS_ASSIGN_LHS-36] - _ = x[TRANS_ASSIGN_RHS-37] - _ = x[TRANS_BLOCK_BODY-38] - _ = x[TRANS_DECL_BODY-39] - _ = x[TRANS_DEFER_CALL-40] - _ = x[TRANS_EXPR_X-41] - _ = x[TRANS_FOR_INIT-42] - _ = x[TRANS_FOR_COND-43] - _ = x[TRANS_FOR_POST-44] - _ = x[TRANS_FOR_BODY-45] - _ = x[TRANS_GO_CALL-46] - _ = x[TRANS_IF_INIT-47] - _ = x[TRANS_IF_COND-48] - _ = x[TRANS_IF_BODY-49] - _ = x[TRANS_IF_ELSE-50] - _ = x[TRANS_IF_CASE_BODY-51] - _ = x[TRANS_INCDEC_X-52] - _ = x[TRANS_RANGE_X-53] - _ = x[TRANS_RANGE_KEY-54] - _ = x[TRANS_RANGE_VALUE-55] - _ = x[TRANS_RANGE_BODY-56] - _ = x[TRANS_RETURN_RESULT-57] - _ = x[TRANS_PANIC_EXCEPTION-58] - _ = x[TRANS_SELECT_CASE-59] - _ = x[TRANS_SELECTCASE_COMM-60] - _ = x[TRANS_SELECTCASE_BODY-61] - _ = x[TRANS_SEND_CHAN-62] - _ = x[TRANS_SEND_VALUE-63] - _ = x[TRANS_SWITCH_INIT-64] - _ = x[TRANS_SWITCH_X-65] - _ = x[TRANS_SWITCH_CASE-66] - _ = x[TRANS_SWITCHCASE_CASE-67] - _ = x[TRANS_SWITCHCASE_BODY-68] - _ = x[TRANS_FUNC_RECV-69] - _ = x[TRANS_FUNC_TYPE-70] - _ = x[TRANS_FUNC_BODY-71] - _ = x[TRANS_IMPORT_PATH-72] - _ = x[TRANS_CONST_TYPE-73] - _ = x[TRANS_CONST_VALUE-74] - _ = x[TRANS_VAR_NAME-75] - _ = x[TRANS_VAR_TYPE-76] - _ = x[TRANS_VAR_VALUE-77] - _ = x[TRANS_TYPE_TYPE-78] - _ = x[TRANS_FILE_BODY-79] -} - -const _TransField_name = "TRANS_ROOTTRANS_BINARY_LEFTTRANS_BINARY_RIGHTTRANS_CALL_FUNCTRANS_CALL_ARGTRANS_INDEX_XTRANS_INDEX_INDEXTRANS_SELECTOR_XTRANS_SLICE_XTRANS_SLICE_LOWTRANS_SLICE_HIGHTRANS_SLICE_MAXTRANS_STAR_XTRANS_REF_XTRANS_TYPEASSERT_XTRANS_TYPEASSERT_TYPETRANS_UNARY_XTRANS_COMPOSITE_TYPETRANS_COMPOSITE_KEYTRANS_COMPOSITE_VALUETRANS_FUNCLIT_TYPETRANS_FUNCLIT_HEAP_CAPTURETRANS_FUNCLIT_BODYTRANS_FIELDTYPE_TYPETRANS_FIELDTYPE_TAGTRANS_ARRAYTYPE_LENTRANS_ARRAYTYPE_ELTTRANS_SLICETYPE_ELTTRANS_INTERFACETYPE_METHODTRANS_CHANTYPE_VALUETRANS_FUNCTYPE_PARAMTRANS_FUNCTYPE_RESULTTRANS_MAPTYPE_KEYTRANS_MAPTYPE_VALUETRANS_STRUCTTYPE_FIELDTRANS_MAYBENATIVETYPE_TYPETRANS_ASSIGN_LHSTRANS_ASSIGN_RHSTRANS_BLOCK_BODYTRANS_DECL_BODYTRANS_DEFER_CALLTRANS_EXPR_XTRANS_FOR_INITTRANS_FOR_CONDTRANS_FOR_POSTTRANS_FOR_BODYTRANS_GO_CALLTRANS_IF_INITTRANS_IF_CONDTRANS_IF_BODYTRANS_IF_ELSETRANS_IF_CASE_BODYTRANS_INCDEC_XTRANS_RANGE_XTRANS_RANGE_KEYTRANS_RANGE_VALUETRANS_RANGE_BODYTRANS_RETURN_RESULTTRANS_PANIC_EXCEPTIONTRANS_SELECT_CASETRANS_SELECTCASE_COMMTRANS_SELECTCASE_BODYTRANS_SEND_CHANTRANS_SEND_VALUETRANS_SWITCH_INITTRANS_SWITCH_XTRANS_SWITCH_CASETRANS_SWITCHCASE_CASETRANS_SWITCHCASE_BODYTRANS_FUNC_RECVTRANS_FUNC_TYPETRANS_FUNC_BODYTRANS_IMPORT_PATHTRANS_CONST_TYPETRANS_CONST_VALUETRANS_VAR_NAMETRANS_VAR_TYPETRANS_VAR_VALUETRANS_TYPE_TYPETRANS_FILE_BODY" - -var _TransField_index = [...]uint16{0, 10, 27, 45, 60, 74, 87, 104, 120, 133, 148, 164, 179, 191, 202, 220, 241, 254, 274, 293, 314, 332, 358, 376, 396, 415, 434, 453, 472, 498, 518, 538, 559, 576, 595, 617, 643, 659, 675, 691, 706, 722, 734, 748, 762, 776, 790, 803, 816, 829, 842, 855, 873, 887, 900, 915, 932, 948, 967, 988, 1005, 1026, 1047, 1062, 1078, 1095, 1109, 1126, 1147, 1168, 1183, 1198, 1213, 1230, 1246, 1263, 1277, 1291, 1306, 1321, 1336} - -func (i TransField) String() string { - if i >= TransField(len(_TransField_index)-1) { - return "TransField(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _TransField_name[_TransField_index[i]:_TransField_index[i+1]] -} diff --git a/gnovm/pkg/gnolang/vptype_string.go b/gnovm/pkg/gnolang/vptype_string.go deleted file mode 100644 index 62a51c3b256..00000000000 --- a/gnovm/pkg/gnolang/vptype_string.go +++ /dev/null @@ -1,48 +0,0 @@ -// Code generated by "stringer -type=VPType ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[VPUverse-0] - _ = x[VPBlock-1] - _ = x[VPField-2] - _ = x[VPValMethod-3] - _ = x[VPPtrMethod-4] - _ = x[VPInterface-5] - _ = x[VPSubrefField-6] - _ = x[VPDerefField-18] - _ = x[VPDerefValMethod-19] - _ = x[VPDerefPtrMethod-20] - _ = x[VPDerefInterface-21] - _ = x[VPNative-32] -} - -const ( - _VPType_name_0 = "VPUverseVPBlockVPFieldVPValMethodVPPtrMethodVPInterfaceVPSubrefField" - _VPType_name_1 = "VPDerefFieldVPDerefValMethodVPDerefPtrMethodVPDerefInterface" - _VPType_name_2 = "VPNative" -) - -var ( - _VPType_index_0 = [...]uint8{0, 8, 15, 22, 33, 44, 55, 68} - _VPType_index_1 = [...]uint8{0, 12, 28, 44, 60} -) - -func (i VPType) String() string { - switch { - case i <= 6: - return _VPType_name_0[_VPType_index_0[i]:_VPType_index_0[i+1]] - case 18 <= i && i <= 21: - i -= 18 - return _VPType_name_1[_VPType_index_1[i]:_VPType_index_1[i+1]] - case i == 32: - return _VPType_name_2 - default: - return "VPType(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/gnovm/pkg/gnolang/word_string.go b/gnovm/pkg/gnolang/word_string.go deleted file mode 100644 index da5fc3d7412..00000000000 --- a/gnovm/pkg/gnolang/word_string.go +++ /dev/null @@ -1,90 +0,0 @@ -// Code generated by "stringer -type=Word ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ILLEGAL-0] - _ = x[NAME-1] - _ = x[INT-2] - _ = x[FLOAT-3] - _ = x[IMAG-4] - _ = x[CHAR-5] - _ = x[STRING-6] - _ = x[ADD-7] - _ = x[SUB-8] - _ = x[MUL-9] - _ = x[QUO-10] - _ = x[REM-11] - _ = x[BAND-12] - _ = x[BOR-13] - _ = x[XOR-14] - _ = x[SHL-15] - _ = x[SHR-16] - _ = x[BAND_NOT-17] - _ = x[ADD_ASSIGN-18] - _ = x[SUB_ASSIGN-19] - _ = x[MUL_ASSIGN-20] - _ = x[QUO_ASSIGN-21] - _ = x[REM_ASSIGN-22] - _ = x[BAND_ASSIGN-23] - _ = x[BOR_ASSIGN-24] - _ = x[XOR_ASSIGN-25] - _ = x[SHL_ASSIGN-26] - _ = x[SHR_ASSIGN-27] - _ = x[BAND_NOT_ASSIGN-28] - _ = x[LAND-29] - _ = x[LOR-30] - _ = x[ARROW-31] - _ = x[INC-32] - _ = x[DEC-33] - _ = x[EQL-34] - _ = x[LSS-35] - _ = x[GTR-36] - _ = x[ASSIGN-37] - _ = x[NOT-38] - _ = x[NEQ-39] - _ = x[LEQ-40] - _ = x[GEQ-41] - _ = x[DEFINE-42] - _ = x[BREAK-43] - _ = x[CASE-44] - _ = x[CHAN-45] - _ = x[CONST-46] - _ = x[CONTINUE-47] - _ = x[DEFAULT-48] - _ = x[DEFER-49] - _ = x[ELSE-50] - _ = x[FALLTHROUGH-51] - _ = x[FOR-52] - _ = x[FUNC-53] - _ = x[GO-54] - _ = x[GOTO-55] - _ = x[IF-56] - _ = x[IMPORT-57] - _ = x[INTERFACE-58] - _ = x[MAP-59] - _ = x[PACKAGE-60] - _ = x[RANGE-61] - _ = x[RETURN-62] - _ = x[SELECT-63] - _ = x[STRUCT-64] - _ = x[SWITCH-65] - _ = x[TYPE-66] - _ = x[VAR-67] -} - -const _Word_name = "ILLEGALNAMEINTFLOATIMAGCHARSTRINGADDSUBMULQUOREMBANDBORXORSHLSHRBAND_NOTADD_ASSIGNSUB_ASSIGNMUL_ASSIGNQUO_ASSIGNREM_ASSIGNBAND_ASSIGNBOR_ASSIGNXOR_ASSIGNSHL_ASSIGNSHR_ASSIGNBAND_NOT_ASSIGNLANDLORARROWINCDECEQLLSSGTRASSIGNNOTNEQLEQGEQDEFINEBREAKCASECHANCONSTCONTINUEDEFAULTDEFERELSEFALLTHROUGHFORFUNCGOGOTOIFIMPORTINTERFACEMAPPACKAGERANGERETURNSELECTSTRUCTSWITCHTYPEVAR" - -var _Word_index = [...]uint16{0, 7, 11, 14, 19, 23, 27, 33, 36, 39, 42, 45, 48, 52, 55, 58, 61, 64, 72, 82, 92, 102, 112, 122, 133, 143, 153, 163, 173, 188, 192, 195, 200, 203, 206, 209, 212, 215, 221, 224, 227, 230, 233, 239, 244, 248, 252, 257, 265, 272, 277, 281, 292, 295, 299, 301, 305, 307, 313, 322, 325, 332, 337, 343, 349, 355, 361, 365, 368} - -func (i Word) String() string { - if i < 0 || i >= Word(len(_Word_index)-1) { - return "Word(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Word_name[_Word_index[i]:_Word_index[i+1]] -} diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index a349ddf092e..01e5d1831dd 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -1,4 +1,4 @@ -// This file is autogenerated from the genstd tool (@/misc/genstd); do not edit. +// Code generated by the genstd tool (@/misc/genstd); DO NOT EDIT. // To regenerate it, run `go generate` from this directory. package stdlibs diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go index 4690f47d82f..d7417b0c77d 100644 --- a/gnovm/tests/stdlibs/generated.go +++ b/gnovm/tests/stdlibs/generated.go @@ -1,4 +1,4 @@ -// This file is autogenerated from the genstd tool (@/misc/genstd); do not edit. +// Code generated by the genstd tool (@/misc/genstd); DO NOT EDIT. // To regenerate it, run `go generate` from this directory. package stdlibs diff --git a/misc/genstd/template.tmpl b/misc/genstd/template.tmpl index 2d714589ef6..09dd7786268 100644 --- a/misc/genstd/template.tmpl +++ b/misc/genstd/template.tmpl @@ -1,4 +1,4 @@ -// This file is autogenerated from the genstd tool (@/misc/genstd); do not edit. +// Code generated by the genstd tool (@/misc/genstd); DO NOT EDIT. // To regenerate it, run `go generate` from this directory. package stdlibs diff --git a/misc/genstd/testdata/integration/generated.go.golden b/misc/genstd/testdata/integration/generated.go.golden index d0be334480f..049adf83e7c 100644 --- a/misc/genstd/testdata/integration/generated.go.golden +++ b/misc/genstd/testdata/integration/generated.go.golden @@ -1,4 +1,4 @@ -// This file is autogenerated from the genstd tool (@/misc/genstd); do not edit. +// Code generated by the genstd tool (@/misc/genstd); DO NOT EDIT. // To regenerate it, run `go generate` from this directory. package stdlibs From d2c383861e01302fb8cee1376d02c1c20fb65eea Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:24:31 +0100 Subject: [PATCH 51/60] feat(gnodev): lazy loading & staging support (#3237) featuring: - [X] **Lazy reload**: Super fast reload, only loading the needed packages for the current directory package. - [x] **Chain resolver**: You can change the gnodev resolving process so it can resolve packages on-chain and configure a local fallback, allowing you to try your package against the chain before submitting it. - This will probably be updated when #2932 & #3123 will be merged - [x] **Staging Mode**: `gnodev` now starts from the current directory by default. The staging subcommand will reproduce previous behaviors by loading and monitoring the entire example folder.
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
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: Morgan Co-authored-by: Morgan --- contribs/gnodev/cmd/gnodev/accounts.go | 2 +- contribs/gnodev/cmd/gnodev/app.go | 582 +++++++++++++++++ contribs/gnodev/cmd/gnodev/app_config.go | 237 +++++++ contribs/gnodev/cmd/gnodev/command_local.go | 114 ++++ contribs/gnodev/cmd/gnodev/command_staging.go | 74 +++ contribs/gnodev/cmd/gnodev/logger.go | 54 +- contribs/gnodev/cmd/gnodev/main.go | 586 +----------------- contribs/gnodev/cmd/gnodev/path_manager.go | 45 ++ .../gnodev/cmd/gnodev/setup_address_book.go | 14 +- contribs/gnodev/cmd/gnodev/setup_loader.go | 107 ++++ contribs/gnodev/cmd/gnodev/setup_node.go | 48 +- contribs/gnodev/cmd/gnodev/setup_term.go | 4 +- contribs/gnodev/cmd/gnodev/setup_web.go | 15 +- contribs/gnodev/go.mod | 2 +- .../mock/{ => emitter}/server_emitter.go | 0 contribs/gnodev/pkg/dev/node.go | 297 +++++---- contribs/gnodev/pkg/dev/node_state.go | 8 +- contribs/gnodev/pkg/dev/node_state_test.go | 26 +- contribs/gnodev/pkg/dev/node_test.go | 269 ++++---- contribs/gnodev/pkg/dev/packages.go | 170 ----- contribs/gnodev/pkg/dev/packages_test.go | 103 --- contribs/gnodev/pkg/dev/query_path.go | 58 ++ contribs/gnodev/pkg/dev/query_path_test.go | 132 ++++ contribs/gnodev/pkg/emitter/server.go | 25 +- .../gnodev/pkg/emitter/static/hotreload.js | 37 +- contribs/gnodev/pkg/logger/log_column.go | 24 +- contribs/gnodev/pkg/packages/glob.go | 214 +++++++ contribs/gnodev/pkg/packages/glob_test.go | 93 +++ contribs/gnodev/pkg/packages/loader.go | 12 + contribs/gnodev/pkg/packages/loader_base.go | 104 ++++ contribs/gnodev/pkg/packages/loader_glob.go | 94 +++ contribs/gnodev/pkg/packages/loader_test.go | 83 +++ contribs/gnodev/pkg/packages/package.go | 102 +++ contribs/gnodev/pkg/packages/resolver.go | 234 +++++++ .../gnodev/pkg/packages/resolver_local.go | 39 ++ contribs/gnodev/pkg/packages/resolver_mock.go | 40 ++ .../gnodev/pkg/packages/resolver_remote.go | 94 +++ .../pkg/packages/resolver_remote_test.go | 1 + contribs/gnodev/pkg/packages/resolver_root.go | 30 + contribs/gnodev/pkg/packages/resolver_test.go | 290 +++++++++ .../testdata/abc.xy/nested/aa/file.gno | 1 + .../testdata/abc.xy/nested/aa/gno.mod | 1 + .../testdata/abc.xy/nested/nested/bb/file.gno | 1 + .../testdata/abc.xy/nested/nested/bb/gno.mod | 1 + .../testdata/abc.xy/nested/nested/cc/file.gno | 1 + .../testdata/abc.xy/nested/nested/cc/gno.mod | 1 + .../packages/testdata/abc.xy/pkg/aa/file.gno | 3 + .../packages/testdata/abc.xy/pkg/aa/gno.mod | 1 + .../packages/testdata/abc.xy/pkg/bb/file.gno | 5 + .../packages/testdata/abc.xy/pkg/bb/gno.mod | 1 + .../packages/testdata/abc.xy/pkg/cc/file.gno | 5 + .../packages/testdata/abc.xy/pkg/cc/gno.mod | 1 + contribs/gnodev/pkg/packages/testdata_test.go | 44 ++ contribs/gnodev/pkg/packages/utils.go | 14 + contribs/gnodev/pkg/proxy/path_interceptor.go | 330 ++++++++++ .../gnodev/pkg/proxy/path_interceptor_test.go | 179 ++++++ contribs/gnodev/pkg/rawterm/keypress.go | 14 + contribs/gnodev/pkg/rawterm/rawterm.go | 25 +- contribs/gnodev/pkg/watcher/watch.go | 66 +- gno.land/pkg/gnoweb/handler.go | 1 + gno.land/pkg/integration/node_testing.go | 29 + gno.land/pkg/integration/node_testing_test.go | 75 +++ gno.land/pkg/keyscli/run.go | 3 +- gnovm/memfile.go | 2 +- gnovm/pkg/gnolang/nodes.go | 3 +- tm2/pkg/commands/command.go | 60 +- 66 files changed, 4097 insertions(+), 1233 deletions(-) create mode 100644 contribs/gnodev/cmd/gnodev/app.go create mode 100644 contribs/gnodev/cmd/gnodev/app_config.go create mode 100644 contribs/gnodev/cmd/gnodev/command_local.go create mode 100644 contribs/gnodev/cmd/gnodev/command_staging.go create mode 100644 contribs/gnodev/cmd/gnodev/path_manager.go create mode 100644 contribs/gnodev/cmd/gnodev/setup_loader.go rename contribs/gnodev/internal/mock/{ => emitter}/server_emitter.go (100%) delete mode 100644 contribs/gnodev/pkg/dev/packages.go delete mode 100644 contribs/gnodev/pkg/dev/packages_test.go create mode 100644 contribs/gnodev/pkg/dev/query_path.go create mode 100644 contribs/gnodev/pkg/dev/query_path_test.go create mode 100644 contribs/gnodev/pkg/packages/glob.go create mode 100644 contribs/gnodev/pkg/packages/glob_test.go create mode 100644 contribs/gnodev/pkg/packages/loader.go create mode 100644 contribs/gnodev/pkg/packages/loader_base.go create mode 100644 contribs/gnodev/pkg/packages/loader_glob.go create mode 100644 contribs/gnodev/pkg/packages/loader_test.go create mode 100644 contribs/gnodev/pkg/packages/package.go create mode 100644 contribs/gnodev/pkg/packages/resolver.go create mode 100644 contribs/gnodev/pkg/packages/resolver_local.go create mode 100644 contribs/gnodev/pkg/packages/resolver_mock.go create mode 100644 contribs/gnodev/pkg/packages/resolver_remote.go create mode 100644 contribs/gnodev/pkg/packages/resolver_remote_test.go create mode 100644 contribs/gnodev/pkg/packages/resolver_root.go create mode 100644 contribs/gnodev/pkg/packages/resolver_test.go create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/file.gno create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/gno.mod create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/file.gno create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/gno.mod create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/file.gno create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/gno.mod create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/file.gno create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/gno.mod create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/file.gno create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/gno.mod create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/file.gno create mode 100644 contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/gno.mod create mode 100644 contribs/gnodev/pkg/packages/testdata_test.go create mode 100644 contribs/gnodev/pkg/packages/utils.go create mode 100644 contribs/gnodev/pkg/proxy/path_interceptor.go create mode 100644 contribs/gnodev/pkg/proxy/path_interceptor_test.go create mode 100644 gno.land/pkg/integration/node_testing_test.go diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go index 95c2c3efffc..e148f4827c1 100644 --- a/contribs/gnodev/cmd/gnodev/accounts.go +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -49,7 +49,7 @@ func (va varPremineAccounts) String() string { return strings.Join(accs, ",") } -func generateBalances(bk *address.Book, cfg *devCfg) (gnoland.Balances, error) { +func generateBalances(bk *address.Book, cfg *AppConfig) (gnoland.Balances, error) { bls := gnoland.NewBalances() premineBalance := std.Coins{std.NewCoin(ugnot.Denom, 10e12)} diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go new file mode 100644 index 00000000000..5744be8d0b4 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -0,0 +1,582 @@ +package main + +import ( + "context" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/contribs/gnodev/pkg/proxy" + "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" + "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + osm "github.com/gnolang/gno/tm2/pkg/os" +) + +const ( + DefaultDeployerName = integration.DefaultAccount_Name + DefaultDeployerSeed = integration.DefaultAccount_Seed +) + +var defaultDeployerAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) + +const ( + NodeLogName = "Node" + WebLogName = "GnoWeb" + KeyPressLogName = "KeyPress" + EventServerLogName = "Event" + AccountsLogName = "Accounts" + LoaderLogName = "Loader" + ProxyLogName = "Proxy" +) + +type App struct { + io commands.IO + start time.Time // Time when the server started + cfg *AppConfig + logger *slog.Logger + pathManager *pathManager + // Contains all the deferred functions of the app. + // Will be triggered on close for cleanup. + deferred func() + + webHomePath string + paths []string + devNode *gnodev.Node + emitterServer *emitter.Server + watcher *watcher.PackageWatcher + loader packages.Loader + book *address.Book + exportPath string + proxy *proxy.PathInterceptor + + // XXX: move this + exported uint +} + +func runApp(cfg *AppConfig, cio commands.IO, dirs ...string) (err error) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var rt *rawterm.RawTerm + var out io.Writer + if cfg.interactive { + var restore func() error + rt, restore, err = setupRawTerm(cfg, cio) + if err != nil { + return fmt.Errorf("unable to init raw term: %w", err) + } + defer restore() + + osm.TrapSignal(func() { + cancel() + restore() + }) + + out = rt + } else { + osm.TrapSignal(cancel) + out = cio.Out() + } + + logger, err := setuplogger(cfg, out) + if err != nil { + return fmt.Errorf("unable to setup logger: %w", err) + } + + app := NewApp(logger, cfg, cio) + if err := app.Setup(ctx, dirs...); err != nil { + return err + } + defer app.Close() + + if rt != nil { + go func() { + app.RunInteractive(ctx, rt) + cancel() + }() + } + + return app.RunServer(ctx, rt) +} + +func NewApp(logger *slog.Logger, cfg *AppConfig, io commands.IO) *App { + return &App{ + start: time.Now(), + deferred: func() {}, + logger: logger, + cfg: cfg, + io: io, + pathManager: newPathManager(), + } +} + +func (ds *App) Defer(fn func()) { + old := ds.deferred + ds.deferred = func() { + defer old() + fn() + } +} + +func (ds *App) DeferClose(fn func() error) { + ds.Defer(func() { + if err := fn(); err != nil { + ds.logger.Debug("close", "error", err.Error()) + } + }) +} + +func (ds *App) Close() { + ds.deferred() +} + +func (ds *App) Setup(ctx context.Context, dirs ...string) (err error) { + if err := ds.cfg.validateConfigFlags(); err != nil { + return fmt.Errorf("validate error: %w", err) + } + + loggerEvents := ds.logger.WithGroup(EventServerLogName) + ds.emitterServer = emitter.NewServer(loggerEvents) + + // XXX: it would be nice to not have this hardcoded + examplesDir := filepath.Join(ds.cfg.root, "examples") + + // Setup loader and resolver + loaderLogger := ds.logger.WithGroup(LoaderLogName) + resolver, localPaths := setupPackagesResolver(loaderLogger, ds.cfg, dirs...) + ds.loader = packages.NewGlobLoader(examplesDir, resolver) + + // Get user's address book from local keybase + accountLogger := ds.logger.WithGroup(AccountsLogName) + ds.book, err = setupAddressBook(accountLogger, ds.cfg) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + + // Generate user's paths using a comma as the delimiter + qpaths := strings.Split(ds.cfg.paths, ",") + + // Set up the packages modifier and extract paths from queries + // XXX: This should probably be moved into the setup node configuration + modifiers, paths, err := resolvePackagesModifier(ds.cfg, ds.book, qpaths) + if err != nil { + return fmt.Errorf("unable to resolve paths %v: %w", paths, err) + } + + // Add the user's paths to the pre-loaded paths + // Modifiers will be added later to the node config bellow + ds.paths = append(paths, localPaths...) + + // Setup default web home realm, fallback on first local path + switch webHome := ds.cfg.webHome; webHome { + case "": + if len(ds.paths) > 0 { + ds.webHomePath = strings.TrimPrefix(ds.paths[0], ds.cfg.chainDomain) + ds.logger.WithGroup(WebLogName).Info("using default package", "path", ds.paths[0]) + } + case "/", ":none:": // skip + default: + ds.webHomePath = webHome + } + + balances, err := generateBalances(ds.book, ds.cfg) + if err != nil { + return fmt.Errorf("unable to generate balances: %w", err) + } + ds.logger.Debug("balances loaded", "list", balances.List()) + + nodeLogger := ds.logger.WithGroup(NodeLogName) + nodeCfg := setupDevNodeConfig(ds.cfg, nodeLogger, ds.emitterServer, balances, ds.loader) + nodeCfg.PackagesModifier = modifiers // add modifiers + + address := resolveUnixOrTCPAddr(nodeCfg.TMConfig.RPC.ListenAddress) + + // Setup lazy proxy + if ds.cfg.lazyLoader { + proxyLogger := ds.logger.WithGroup(ProxyLogName) + ds.proxy, err = proxy.NewPathInterceptor(proxyLogger, address) + if err != nil { + return fmt.Errorf("unable to setup proxy: %w", err) + } + ds.DeferClose(ds.proxy.Close) + + // Override current rpc listener + nodeCfg.TMConfig.RPC.ListenAddress = ds.proxy.ProxyAddress() + proxyLogger.Debug("proxy started", + "proxy_addr", ds.proxy.ProxyAddress(), + "target_addr", ds.proxy.TargetAddress(), + ) + + proxyLogger.Info("lazy loading is enabled. packages will be loaded only upon a request via a query or transaction.", "loader", ds.loader.Name()) + } else { + nodeCfg.TMConfig.RPC.ListenAddress = fmt.Sprintf("%s://%s", address.Network(), address.String()) + } + + ds.devNode, err = setupDevNode(ctx, ds.cfg, nodeCfg, ds.paths...) + if err != nil { + return err + } + ds.DeferClose(ds.devNode.Close) + + ds.watcher, err = watcher.NewPackageWatcher(loggerEvents, ds.emitterServer) + if err != nil { + return fmt.Errorf("unable to setup packages watcher: %w", err) + } + + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + + return nil +} + +func (ds *App) setupHandlers(ctx context.Context) (http.Handler, error) { + mux := http.NewServeMux() + remote := ds.devNode.GetRemoteAddress() + + if ds.proxy != nil { + proxyLogger := ds.logger.WithGroup(ProxyLogName) + remote = ds.proxy.TargetAddress() // update remote address with proxy target address + + // Generate initial paths + initPaths := map[string]struct{}{} + for _, pkg := range ds.devNode.ListPkgs() { + initPaths[pkg.Path] = struct{}{} + } + + ds.proxy.HandlePath(func(paths ...string) { + newPath := false + for _, path := range paths { + // Check if the path is an initial path. + if _, ok := initPaths[path]; ok { + continue + } + + // Try to resolve the path first. + // If we are unable to resolve it, ignore and continue + + if _, err := ds.loader.Resolve(path); err != nil { + proxyLogger.Debug("unable to resolve path", + "error", err, + "path", path) + continue + } + + // If we already know this path, continue. + if exist := ds.pathManager.Save(path); exist { + continue + } + + proxyLogger.Info("new monitored path", + "path", path) + + newPath = true + } + + if !newPath { + return + } + + ds.emitterServer.LockEmit() + defer ds.emitterServer.UnlockEmit() + + ds.devNode.SetPackagePaths(ds.paths...) + ds.devNode.AddPackagePaths(ds.pathManager.List()...) + + // Check if the node needs to be reloaded + // XXX: This part can likely be optimized if we believe + // it significantly impacts performance. + for _, path := range paths { + if ds.devNode.HasPackageLoaded(path) { + continue + } + + ds.logger.WithGroup(NodeLogName).Debug("some paths aren't loaded yet", "path", path) + + // If the package isn't loaded, attempt to reload the node + if err := ds.devNode.Reload(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + + // Update the watcher list with the currently loaded packages + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + + // Reloading the node once is sufficient, so exit the loop + return + } + + ds.logger.WithGroup(NodeLogName).Debug("paths already loaded, skipping reload", "paths", paths) + }) + } + + // Setup gnoweb + webhandler, err := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, remote) + if err != nil { + return nil, fmt.Errorf("unable to setup gnoweb server: %w", err) + } + + if ds.webHomePath != "" { + serveWeb := webhandler.ServeHTTP + webhandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "" || r.URL.Path == "/" { + http.Redirect(w, r, ds.webHomePath, http.StatusFound) + } else { + serveWeb(w, r) + } + }) + } + + // Setup unsafe API + if ds.cfg.unsafeAPI { + mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { + if err := ds.devNode.Reset(req.Context()); err != nil { + ds.logger.Error("failed to reset", slog.Any("err", err)) + res.WriteHeader(http.StatusInternalServerError) + } + }) + + mux.HandleFunc("/reload", func(res http.ResponseWriter, req *http.Request) { + if err := ds.devNode.Reload(req.Context()); err != nil { + ds.logger.Error("failed to reload", slog.Any("err", err)) + res.WriteHeader(http.StatusInternalServerError) + } + }) + } + + if !ds.cfg.noWatch { + evtstarget := fmt.Sprintf("%s/_events", ds.cfg.webListenerAddr) + mux.Handle("/_events", ds.emitterServer) + mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) + } else { + mux.Handle("/", webhandler) + } + + return mux, nil +} + +func (ds *App) RunServer(ctx context.Context, term *rawterm.RawTerm) error { + ctx, cancelWith := context.WithCancelCause(ctx) + defer cancelWith(nil) + + addr := ds.cfg.webListenerAddr + handlers, err := ds.setupHandlers(ctx) + if err != nil { + return fmt.Errorf("unable to setup handlers: %w", err) + } + + server := &http.Server{ + Handler: handlers, + Addr: addr, + ReadHeaderTimeout: 60 * time.Second, + } + + // Serve gnoweb + if !ds.cfg.noWeb { + go func() { + err := server.ListenAndServe() + cancelWith(err) + }() + + ds.logger.WithGroup(WebLogName).Info("gnoweb started", + "lisn", fmt.Sprintf("http://%s", addr)) + } + + if ds.cfg.interactive { + ds.logger.WithGroup("--- READY").Info("for commands and help, press `h`", "took", time.Since(ds.start)) + } else { + ds.logger.Info("node is ready", "took", time.Since(ds.start)) + } + + for { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case _, ok := <-ds.watcher.PackagesUpdate: + if !ok { + return nil + } + + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err := ds.devNode.Reload(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + } + } +} + +func (ds *App) RunInteractive(ctx context.Context, term *rawterm.RawTerm) { + ds.logger.WithGroup(KeyPressLogName).Debug("starting interactive mode") + var keyPressCh <-chan rawterm.KeyPress + if ds.cfg.interactive { + keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) + } + + for { + select { + case <-ctx.Done(): + return + case key, ok := <-keyPressCh: + ds.logger.WithGroup(KeyPressLogName).Debug("pressed", "key", key.String()) + if !ok { + return + } + + if key == rawterm.KeyCtrlC { + return + } + + ds.handleKeyPress(ctx, key) + keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) + } + } +} + +var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: +https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev + +P Previous TX - Go to the previous tx +N Next TX - Go to the next tx +E Export - Export the current state as genesis doc +A Accounts - Display known accounts and balances +H Help - Display this message +R Reload - Reload all packages to take change into account. +Ctrl+S Save State - Save the current state +Ctrl+R Reset - Reset application to it's initial/save state. +Ctrl+C Exit - Exit the application +` + +func (ds *App) handleKeyPress(ctx context.Context, key rawterm.KeyPress) { + var err error + + switch key.Upper() { + case rawterm.KeyH: // Helper + ds.logger.Info("Gno Dev Helper", "helper", helper) + + case rawterm.KeyA: // Accounts + logAccounts(ds.logger.WithGroup(AccountsLogName), ds.book, ds.devNode) + + case rawterm.KeyR: // Reload + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err = ds.devNode.ReloadAll(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + + case rawterm.KeyCtrlR: // Reset + ds.logger.WithGroup(NodeLogName).Info("resetting node state...") + // Reset paths + ds.pathManager.Reset() + ds.devNode.SetPackagePaths(ds.paths...) + // Reset the node + if err = ds.devNode.Reset(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reset node state", "err", err) + } + + case rawterm.KeyCtrlS: // Save + ds.logger.WithGroup(NodeLogName).Info("saving state...") + if err := ds.devNode.SaveCurrentState(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to save node state", "err", err) + } + + case rawterm.KeyE: // Export + // Create a temporary export dir + if ds.exported == 0 { + ds.exportPath, err = os.MkdirTemp("", "gnodev-export") + if err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to create `export` directory", "err", err) + return + } + } + ds.exported++ + + ds.logger.WithGroup(NodeLogName).Info("exporting state...") + doc, err := ds.devNode.ExportStateAsGenesis(ctx) + if err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to export node state", "err", err) + return + } + + docfile := filepath.Join(ds.exportPath, fmt.Sprintf("export_%d.jsonl", ds.exported)) + if err := doc.SaveAs(docfile); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to save genesis", "err", err) + } + + ds.logger.WithGroup(NodeLogName).Info("node state exported", "file", docfile) + + case rawterm.KeyN: // Next tx + ds.logger.Info("moving forward...") + if err := ds.devNode.MoveToNextTX(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to move forward", "err", err) + } + + case rawterm.KeyP: // Previous tx + ds.logger.Info("moving backward...") + if err := ds.devNode.MoveToPreviousTX(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to move backward", "err", err) + } + default: + } +} + +// XXX: packages modifier does not support glob yet +func resolvePackagesModifier(cfg *AppConfig, bk *address.Book, qpaths []string) ([]gnodev.QueryPath, []string, error) { + if cfg.deployKey == "" { + return nil, nil, fmt.Errorf("default deploy key cannot be empty") + } + + defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) + if !ok { + return nil, nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) + } + + modifiers := make([]gnodev.QueryPath, 0, len(qpaths)) + paths := make([]string, 0, len(qpaths)) + + for _, path := range qpaths { + if path == "" { + continue + } + + qpath, err := gnodev.ResolveQueryPath(bk, path) + if err != nil { + return nil, nil, fmt.Errorf("invalid package path/query %q: %w", path, err) + } + + // Assign a default creator if user haven't specified it. + if qpath.Creator.IsZero() { + qpath.Creator = defaultKey + } + + modifiers = append(modifiers, qpath) + paths = append(paths, qpath.Path) + } + + return slices.Clip(modifiers), slices.Clip(paths), nil +} + +func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { + cc := make(chan rawterm.KeyPress, 1) + go func() { + defer close(cc) + key, err := rt.ReadKeyPress() + if err != nil { + logger.Error("unable to read keypress", "err", err) + return + } + + cc <- key + }() + + return cc +} diff --git a/contribs/gnodev/cmd/gnodev/app_config.go b/contribs/gnodev/cmd/gnodev/app_config.go new file mode 100644 index 00000000000..07231f24f9b --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/app_config.go @@ -0,0 +1,237 @@ +package main + +import "flag" + +type AppConfig struct { + // Listeners + nodeRPCListenerAddr string + nodeP2PListenerAddr string + nodeProxyAppListenerAddr string + + // Users default + deployKey string + home string + root string + premineAccounts varPremineAccounts + + // Files + balancesFile string + genesisFile string + txsFile string + + // Web Configuration + noWeb bool + webHTML bool + webListenerAddr string + webRemoteHelperAddr string + webWithHTML bool + webHome string + + // Resolver + resolvers varResolver + + // Node Configuration + logFormat string + lazyLoader bool + verbose bool + noWatch bool + noReplay bool + maxGas int64 + chainId string + chainDomain string + unsafeAPI bool + interactive bool + paths string +} + +func (c *AppConfig) RegisterFlagsWith(fs *flag.FlagSet, defaultCfg AppConfig) { + *c = defaultCfg // Copy default config + + fs.StringVar( + &c.home, + "home", + defaultCfg.home, + "user's local directory for keys", + ) + + fs.BoolVar( + &c.interactive, + "interactive", + defaultCfg.interactive, + "enable gnodev interactive mode", + ) + + fs.StringVar( + &c.root, + "root", + defaultCfg.root, + "gno root directory", + ) + + fs.BoolVar( + &c.noWeb, + "no-web", + defaultLocalAppConfig.noWeb, + "disable gnoweb", + ) + + fs.BoolVar( + &c.webHTML, + "web-html", + defaultLocalAppConfig.webHTML, + "gnoweb: enable unsafe HTML parsing in markdown rendering", + ) + + fs.StringVar( + &c.webListenerAddr, + "web-listener", + defaultCfg.webListenerAddr, + "gnoweb: web server listener address", + ) + + fs.StringVar( + &c.webRemoteHelperAddr, + "web-help-remote", + defaultCfg.webRemoteHelperAddr, + "gnoweb: web server help page's remote addr (default to )", + ) + + fs.BoolVar( + &c.webWithHTML, + "web-with-html", + defaultCfg.webWithHTML, + "gnoweb: enable HTML parsing in markdown rendering", + ) + + fs.StringVar( + &c.webHome, + "web-home", + defaultCfg.webHome, + "gnoweb: set default home page, use `/` or `:none:` to use default web home redirect", + ) + + fs.Var( + &c.resolvers, + "resolver", + "list of additional resolvers (`root`, `local` or `remote`), will be executed in the given order", + ) + + fs.StringVar( + &c.nodeRPCListenerAddr, + "node-rpc-listener", + defaultCfg.nodeRPCListenerAddr, + "listening address for GnoLand RPC node", + ) + + fs.Var( + &c.premineAccounts, + "add-account", + "add (or set) a premine account in the form `[=]`, can be used multiple time", + ) + + fs.StringVar( + &c.balancesFile, + "balance-file", + defaultCfg.balancesFile, + "load the provided balance file (refer to the documentation for format)", + ) + + fs.StringVar( + &c.txsFile, + "txs-file", + defaultCfg.txsFile, + "load the provided transactions file (refer to the documentation for format)", + ) + + fs.StringVar( + &c.genesisFile, + "genesis", + defaultCfg.genesisFile, + "load the given genesis file", + ) + + fs.StringVar( + &c.deployKey, + "deploy-key", + defaultCfg.deployKey, + "default key name or Bech32 address for deploying packages", + ) + + fs.StringVar( + &c.chainId, + "chain-id", + defaultCfg.chainId, + "set node ChainID", + ) + + fs.StringVar( + &c.chainDomain, + "chain-domain", + defaultCfg.chainDomain, + "set node ChainDomain", + ) + + fs.BoolVar( + &c.noWatch, + "no-watch", + defaultCfg.noWatch, + "do not watch for file changes", + ) + + fs.BoolVar( + &c.noReplay, + "no-replay", + defaultCfg.noReplay, + "do not replay previous transactions upon reload", + ) + + fs.BoolVar( + &c.lazyLoader, + "lazy-loader", + defaultCfg.lazyLoader, + "enable lazy loader", + ) + + fs.Int64Var( + &c.maxGas, + "max-gas", + defaultCfg.maxGas, + "set the maximum gas per block", + ) + + fs.BoolVar( + &c.unsafeAPI, + "unsafe-api", + defaultCfg.unsafeAPI, + "enable /reset and /reload endpoints which are not safe to expose publicly", + ) + + fs.StringVar( + &c.logFormat, + "log-format", + defaultCfg.logFormat, + "log output format, can be `json` or `console`", + ) + + fs.StringVar( + &c.paths, + "paths", + defaultCfg.paths, + "additional path(s) to load, separated by comma", + ) + + fs.BoolVar( + &c.verbose, + "v", + defaultCfg.verbose, + "enable verbose output for development", + ) +} + +func (c *AppConfig) validateConfigFlags() error { + if (c.balancesFile != "" || c.txsFile != "") && c.genesisFile != "" { + return ErrConflictingFileArgs + } + + return nil +} diff --git a/contribs/gnodev/cmd/gnodev/command_local.go b/contribs/gnodev/cmd/gnodev/command_local.go new file mode 100644 index 00000000000..2a1ccfa063d --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/command_local.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/mattn/go-isatty" +) + +const DefaultDomain = "gno.land" + +var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") + +type LocalAppConfig struct { + AppConfig + + chdir string // directory context +} + +var defaultLocalAppConfig = AppConfig{ + chainId: "dev", + logFormat: "console", + chainDomain: DefaultDomain, + maxGas: 10_000_000_000, + webListenerAddr: "127.0.0.1:8888", + nodeRPCListenerAddr: "127.0.0.1:26657", + deployKey: defaultDeployerAddress.String(), + home: gnoenv.HomeDir(), + root: gnoenv.RootDir(), + interactive: isatty.IsTerminal(os.Stdout.Fd()), + unsafeAPI: true, + lazyLoader: true, + + // As we have no reason to configure this yet, set this to random port + // to avoid potential conflict with other app + nodeP2PListenerAddr: "tcp://127.0.0.1:0", + nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", +} + +func NewLocalCmd(io commands.IO) *commands.Command { + var cfg LocalAppConfig + + return commands.NewCommand( + commands.Metadata{ + Name: "local", + ShortUsage: "gnodev local [flags] [package_dir...]", + ShortHelp: "Start gnodev in local development mode (default)", + LongHelp: "LOCAL: Local mode configure the node for local development usage", + NoParentFlags: true, + }, + &cfg, + func(_ context.Context, args []string) error { + return execLocalApp(&cfg, args, io) + }, + ) +} + +func (c *LocalAppConfig) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.chdir, + "C", + c.chdir, + "change directory context before running gnodev", + ) + + c.AppConfig.RegisterFlagsWith(fs, defaultLocalAppConfig) +} + +func execLocalApp(cfg *LocalAppConfig, args []string, cio commands.IO) error { + if cfg.chdir != "" { + if err := os.Chdir(cfg.chdir); err != nil { + return fmt.Errorf("unable to change directory: %w", err) + } + } + + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("unable to guess current dir: %w", err) + } + + // If no resolvers is defined, use gno example as root resolver + var baseResolvers []packages.Resolver + if len(cfg.resolvers) == 0 { + gnoroot, err := gnoenv.GuessRootDir() + if err != nil { + return err + } + + exampleRoot := filepath.Join(gnoroot, "examples") + baseResolvers = append(baseResolvers, packages.NewRootResolver(exampleRoot)) + } + + // Check if current directory is a valid gno package + path := guessPath(&cfg.AppConfig, dir) + resolver := packages.NewLocalResolver(path, dir) + if resolver.IsValid() { + // Add current directory as local resolver + baseResolvers = append([]packages.Resolver{resolver}, baseResolvers...) + if len(cfg.paths) > 0 { + cfg.paths += "," + } + cfg.paths += resolver.Path + } + cfg.resolvers = append(baseResolvers, cfg.resolvers...) + + return runApp(&cfg.AppConfig, cio) // else run app without any dir +} diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go new file mode 100644 index 00000000000..7b1a0ab3f5a --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "flag" + "path/filepath" + + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type StagingAppConfig struct { + AppConfig +} + +var defaultStagingOptions = AppConfig{ + chainId: "dev", + chainDomain: DefaultDomain, + logFormat: "json", + maxGas: 10_000_000_000, + webHome: ":none:", + webListenerAddr: "127.0.0.1:8888", + nodeRPCListenerAddr: "127.0.0.1:26657", + deployKey: defaultDeployerAddress.String(), + home: gnoenv.HomeDir(), + root: gnoenv.RootDir(), + interactive: false, + unsafeAPI: false, + lazyLoader: false, + paths: filepath.Join(DefaultDomain, "/**"), // Load every package under the main domain}, + + // As we have no reason to configure this yet, set this to random port + // to avoid potential conflict with other app + nodeP2PListenerAddr: "tcp://127.0.0.1:0", + nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", +} + +func NewStagingCmd(io commands.IO) *commands.Command { + var cfg StagingAppConfig + + return commands.NewCommand( + commands.Metadata{ + Name: "staging", + ShortUsage: "gnodev staging [flags] [package_dir...]", + ShortHelp: "Start gnodev in staging mode", + LongHelp: "STAGING: Staging mode configure the node for server usage", + NoParentFlags: true, + }, + &cfg, + func(_ context.Context, args []string) error { + return execStagingCmd(&cfg, args, io) + }, + ) +} + +func (c *StagingAppConfig) RegisterFlags(fs *flag.FlagSet) { + c.AppConfig.RegisterFlagsWith(fs, defaultStagingOptions) +} + +func execStagingCmd(cfg *StagingAppConfig, args []string, io commands.IO) error { + // If no resolvers is defined, use gno example as root resolver + if len(cfg.AppConfig.resolvers) == 0 { + gnoroot, err := gnoenv.GuessRootDir() + if err != nil { + return err + } + + exampleRoot := filepath.Join(gnoroot, "examples") + cfg.AppConfig.resolvers = append(cfg.AppConfig.resolvers, packages.NewRootResolver(exampleRoot)) + } + + return runApp(&cfg.AppConfig, io, args...) +} diff --git a/contribs/gnodev/cmd/gnodev/logger.go b/contribs/gnodev/cmd/gnodev/logger.go index 9e69654f478..1fbcd95e953 100644 --- a/contribs/gnodev/cmd/gnodev/logger.go +++ b/contribs/gnodev/cmd/gnodev/logger.go @@ -1,35 +1,59 @@ package main import ( + "fmt" "io" "log/slog" "github.com/charmbracelet/lipgloss" "github.com/gnolang/gno/contribs/gnodev/pkg/logger" - gnolog "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/gnolang/gno/gno.land/pkg/log" "github.com/muesli/termenv" + "go.uber.org/zap/zapcore" ) -func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { +func setuplogger(cfg *AppConfig, out io.Writer) (*slog.Logger, error) { level := slog.LevelInfo if cfg.verbose { level = slog.LevelDebug } - if cfg.serverMode { - zaplogger := logger.NewZapLogger(out, level) - return gnolog.ZapLoggerToSlog(zaplogger) - } + // Set up the logger + switch cfg.logFormat { + case "json": + return newJSONLogger(out, level), nil + case "console", "": + // Detect term color profile + colorProfile := termenv.DefaultOutput().Profile + + clogger := logger.NewColumnLogger(out, level, colorProfile) + + // Register well known group color with system colors + clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) + clogger.RegisterGroupColor(WebLogName, lipgloss.Color("4")) + clogger.RegisterGroupColor(KeyPressLogName, lipgloss.Color("5")) + clogger.RegisterGroupColor(EventServerLogName, lipgloss.Color("6")) - // Detect term color profile - colorProfile := termenv.DefaultOutput().Profile - clogger := logger.NewColumnLogger(out, level, colorProfile) + return slog.New(clogger), nil + default: + return nil, fmt.Errorf("invalid log format %q", cfg.logFormat) + } +} - // Register well known group color with system colors - clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) - clogger.RegisterGroupColor(WebLogName, lipgloss.Color("4")) - clogger.RegisterGroupColor(KeyPressLogName, lipgloss.Color("5")) - clogger.RegisterGroupColor(EventServerLogName, lipgloss.Color("6")) +func newJSONLogger(w io.Writer, level slog.Level) *slog.Logger { + var zaplevel zapcore.Level + switch level { + case slog.LevelDebug: + zaplevel = zapcore.DebugLevel + case slog.LevelInfo: + zaplevel = zapcore.InfoLevel + case slog.LevelWarn: + zaplevel = zapcore.WarnLevel + case slog.LevelError: + zaplevel = zapcore.ErrorLevel + default: + panic("unknown slog level: " + level.String()) + } - return slog.New(clogger) + return log.ZapLoggerToSlog(log.NewZapJSONLogger(w, zaplevel)) } diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 95f1d95e0a6..a14f76e9d81 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -1,586 +1,60 @@ package main import ( + "bytes" "context" "errors" "flag" "fmt" - "log/slog" - "net/http" "os" - "path/filepath" - "time" - "github.com/gnolang/gno/contribs/gnodev/pkg/address" - gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" - "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" - "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" - "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" - "github.com/gnolang/gno/gno.land/pkg/integration" - "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" - osm "github.com/gnolang/gno/tm2/pkg/os" ) -const ( - NodeLogName = "Node" - WebLogName = "GnoWeb" - KeyPressLogName = "KeyPress" - EventServerLogName = "Event" - AccountsLogName = "Accounts" -) - -var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") - -var ( - DefaultDeployerName = integration.DefaultAccount_Name - DefaultDeployerAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) - DefaultDeployerSeed = integration.DefaultAccount_Seed -) - -type devCfg struct { - // Listeners - nodeRPCListenerAddr string - nodeP2PListenerAddr string - nodeProxyAppListenerAddr string - - // Users default - deployKey string - home string - root string - premineAccounts varPremineAccounts - - // Files - balancesFile string - genesisFile string - txsFile string - - // Web Configuration - noWeb bool - webHTML bool - webListenerAddr string - webRemoteHelperAddr string - - // Node Configuration - minimal bool - verbose bool - noWatch bool - noReplay bool - maxGas int64 - chainId string - chainDomain string - serverMode bool - unsafeAPI bool -} - -var defaultDevOptions = &devCfg{ - chainId: "dev", - chainDomain: "gno.land", - maxGas: 10_000_000_000, - webListenerAddr: "127.0.0.1:8888", - nodeRPCListenerAddr: "127.0.0.1:26657", - deployKey: DefaultDeployerAddress.String(), - home: gnoenv.HomeDir(), - root: gnoenv.RootDir(), - - // As we have no reason to configure this yet, set this to random port - // to avoid potential conflict with other app - nodeP2PListenerAddr: "tcp://127.0.0.1:0", - nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", -} - func main() { - cfg := &devCfg{} - stdio := commands.NewDefaultIO() + + localcmd := NewLocalCmd(stdio) // default + cmd := commands.NewCommand( commands.Metadata{ Name: "gnodev", - ShortUsage: "gnodev [flags] [path ...]", - ShortHelp: "runs an in-memory node and gno.land web server for development purposes.", - LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface primarily for realm package development. It automatically loads the 'examples' directory and any additional specified paths.`, - }, - cfg, - func(_ context.Context, args []string) error { - return execDev(cfg, args, stdio) - }) - - cmd.Execute(context.Background(), os.Args[1:]) -} - -func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.home, - "home", - defaultDevOptions.home, - "user's local directory for keys", - ) - - fs.StringVar( - &c.root, - "root", - defaultDevOptions.root, - "gno root directory", - ) - - fs.BoolVar( - &c.noWeb, - "no-web", - defaultDevOptions.noWeb, - "disable gnoweb", - ) - - fs.BoolVar( - &c.webHTML, - "web-html", - defaultDevOptions.webHTML, - "gnoweb: enable unsafe HTML parsing in markdown rendering", - ) - - fs.StringVar( - &c.webListenerAddr, - "web-listener", - defaultDevOptions.webListenerAddr, - "gnoweb: web server listener address", - ) - - fs.StringVar( - &c.webRemoteHelperAddr, - "web-help-remote", - defaultDevOptions.webRemoteHelperAddr, - "gnoweb: web server help page's remote addr (default to )", - ) - - fs.StringVar( - &c.nodeRPCListenerAddr, - "node-rpc-listener", - defaultDevOptions.nodeRPCListenerAddr, - "listening address for GnoLand RPC node", - ) - - fs.Var( - &c.premineAccounts, - "add-account", - "add (or set) a premine account in the form `[=]`, can be used multiple time", - ) - - fs.StringVar( - &c.balancesFile, - "balance-file", - defaultDevOptions.balancesFile, - "load the provided balance file (refer to the documentation for format)", - ) - - fs.StringVar( - &c.txsFile, - "txs-file", - defaultDevOptions.txsFile, - "load the provided transactions file (refer to the documentation for format)", - ) - - fs.StringVar( - &c.genesisFile, - "genesis", - defaultDevOptions.genesisFile, - "load the given genesis file", - ) - - fs.StringVar( - &c.deployKey, - "deploy-key", - defaultDevOptions.deployKey, - "default key name or Bech32 address for deploying packages", - ) + ShortUsage: "gnodev [flags] ", + ShortHelp: "Runs an in-memory node and gno.land web server for development purposes.", + LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface, primarily for realm package development. - fs.BoolVar( - &c.minimal, - "minimal", - defaultDevOptions.minimal, - "do not load packages from the examples directory", - ) - - fs.BoolVar( - &c.serverMode, - "server-mode", - defaultDevOptions.serverMode, - "disable interaction, and adjust logging for server use.", - ) - - fs.BoolVar( - &c.verbose, - "v", - defaultDevOptions.verbose, - "enable verbose output for development", - ) - - fs.StringVar( - &c.chainId, - "chain-id", - defaultDevOptions.chainId, - "set node ChainID", - ) - - fs.StringVar( - &c.chainDomain, - "chain-domain", - defaultDevOptions.chainDomain, - "set node ChainDomain", - ) - - fs.BoolVar( - &c.noWatch, - "no-watch", - defaultDevOptions.noWatch, - "do not watch for file changes", - ) - - fs.BoolVar( - &c.noReplay, - "no-replay", - defaultDevOptions.noReplay, - "do not replay previous transactions upon reload", - ) - - fs.Int64Var( - &c.maxGas, - "max-gas", - defaultDevOptions.maxGas, - "set the maximum gas per block", - ) - - fs.BoolVar( - &c.unsafeAPI, - "unsafe-api", - defaultDevOptions.unsafeAPI, - "enable /reset and /reload endpoints which are not safe to expose publicly", +If no command is provided, gnodev will automatically start in mode. +For more information and flags usage description, use 'gnodev local -h'.`, + }, + nil, + func(ctx context.Context, _ []string) error { + localcmd.Execute(ctx, os.Args[1:]) + return nil + }, ) -} - -func (c *devCfg) validateConfigFlags() error { - if (c.balancesFile != "" || c.txsFile != "") && c.genesisFile != "" { - return ErrConflictingFileArgs - } - - return nil -} - -func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { - ctx, cancel := context.WithCancelCause(context.Background()) - defer cancel(nil) - - if err := cfg.validateConfigFlags(); err != nil { - return fmt.Errorf("validate error: %w", err) - } - // Setup Raw Terminal - rt, restore, err := setupRawTerm(cfg, io) - if err != nil { - return fmt.Errorf("unable to init raw term: %w", err) - } - defer restore() - - // Setup trap signal - osm.TrapSignal(func() { - cancel(nil) - restore() - }) - - logger := setuplogger(cfg, rt) - loggerEvents := logger.WithGroup(EventServerLogName) - emitterServer := emitter.NewServer(loggerEvents) - - // load keybase - book, err := setupAddressBook(logger.WithGroup(AccountsLogName), cfg) - if err != nil { - return fmt.Errorf("unable to load keybase: %w", err) - } - - // Check and Parse packages - pkgpaths, err := resolvePackagesPathFromArgs(cfg, book, args) - if err != nil { - return fmt.Errorf("unable to parse package paths: %w", err) - } - - // generate balances - balances, err := generateBalances(book, cfg) - if err != nil { - return fmt.Errorf("unable to generate balances: %w", err) - } - logger.Debug("balances loaded", "list", balances.List()) - - // Setup Dev Node - // XXX: find a good way to export or display node logs - nodeLogger := logger.WithGroup(NodeLogName) - nodeCfg := setupDevNodeConfig(cfg, logger, emitterServer, balances, pkgpaths) - devNode, err := setupDevNode(ctx, cfg, nodeCfg) - if err != nil { - return err - } - defer devNode.Close() - - nodeLogger.Info("node started", "lisn", devNode.GetRemoteAddress(), "chainID", cfg.chainId) - - // Create server - mux := http.NewServeMux() - server := http.Server{ - Handler: mux, - Addr: cfg.webListenerAddr, - ReadHeaderTimeout: time.Second * 60, - } - defer server.Close() - - // Setup gnoweb - webhandler, err := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) - if err != nil { - return fmt.Errorf("unable to setup gnoweb server: %w", err) - } - - // Setup unsafe APIs if enabled - if cfg.unsafeAPI { - mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { - if err := devNode.Reset(req.Context()); err != nil { - logger.Error("failed to reset", slog.Any("err", err)) - res.WriteHeader(http.StatusInternalServerError) - } - }) - - mux.HandleFunc("/reload", func(res http.ResponseWriter, req *http.Request) { - if err := devNode.Reload(req.Context()); err != nil { - logger.Error("failed to reload", slog.Any("err", err)) - res.WriteHeader(http.StatusInternalServerError) - } - }) - } - - // Setup HotReload if needed - if !cfg.noWatch { - evtstarget := fmt.Sprintf("%s/_events", server.Addr) - mux.Handle("/_events", emitterServer) - mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) - } else { - mux.Handle("/", webhandler) - } - - // Serve gnoweb - if !cfg.noWeb { - go func() { - err := server.ListenAndServe() - cancel(err) - }() - - logger.WithGroup(WebLogName). - Info("gnoweb started", - "lisn", fmt.Sprintf("http://%s", server.Addr)) - } - - watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer) - if err != nil { - return fmt.Errorf("unable to setup packages watcher: %w", err) - } - defer watcher.Stop() - - // Add node pkgs to watcher - watcher.AddPackages(devNode.ListPkgs()...) - - if !cfg.serverMode { - logger.WithGroup("--- READY").Info("for commands and help, press `h`") - } - - // Run the main event loop - return runEventLoop(ctx, logger, book, rt, devNode, watcher) -} - -var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: -https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev - -P Previous TX - Go to the previous tx -N Next TX - Go to the next tx -E Export - Export the current state as genesis doc -A Accounts - Display known accounts and balances -H Help - Display this message -R Reload - Reload all packages to take change into account. -Ctrl+S Save State - Save the current state -Ctrl+R Reset - Reset application to it's initial/save state. -Ctrl+C Exit - Exit the application -` - -func runEventLoop( - ctx context.Context, - logger *slog.Logger, - bk *address.Book, - rt *rawterm.RawTerm, - dnode *gnodev.Node, - watch *watcher.PackageWatcher, -) error { - // XXX: move this in above, but we need to have a proper struct first - // XXX: make this configurable - var exported uint - path, err := os.MkdirTemp("", "gnodev-export") - if err != nil { - return fmt.Errorf("unable to create `export` directory: %w", err) - } + cmd.AddSubCommands(localcmd) + cmd.AddSubCommands(NewStagingCmd(stdio)) - defer func() { - if exported == 0 { - _ = os.RemoveAll(path) - } - }() - - keyPressCh := listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) - for { - var err error - - select { - case <-ctx.Done(): - return context.Cause(ctx) - case pkgs, ok := <-watch.PackagesUpdate: - if !ok { - return nil - } - - // fmt.Fprintln(nodeOut, "Loading package updates...") - if err = dnode.UpdatePackages(pkgs.PackagesPath()...); err != nil { - return fmt.Errorf("unable to update packages: %w", err) - } - - logger.WithGroup(NodeLogName).Info("reloading...") - if err = dnode.Reload(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reload node", "err", err) - } - - case key, ok := <-keyPressCh: - if !ok { - return nil - } - - logger.WithGroup(KeyPressLogName).Debug( - fmt.Sprintf("<%s>", key.String()), - ) - - switch key.Upper() { - case rawterm.KeyH: // Helper - logger.Info("Gno Dev Helper", "helper", helper) - - case rawterm.KeyA: // Accounts - logAccounts(logger.WithGroup(AccountsLogName), bk, dnode) - - case rawterm.KeyR: // Reload - logger.WithGroup(NodeLogName).Info("reloading...") - if err = dnode.ReloadAll(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reload node", "err", err) - } - - case rawterm.KeyCtrlR: // Reset - logger.WithGroup(NodeLogName).Info("reseting node state...") - if err = dnode.Reset(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reset node state", "err", err) - } - - case rawterm.KeyCtrlS: // Save - logger.WithGroup(NodeLogName).Info("saving state...") - if err := dnode.SaveCurrentState(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to save node state", "err", err) - } - - case rawterm.KeyE: - logger.WithGroup(NodeLogName).Info("exporting state...") - doc, err := dnode.ExportStateAsGenesis(ctx) - if err != nil { - logger.WithGroup(NodeLogName). - Error("unable to export node state", "err", err) - continue - } - - docfile := filepath.Join(path, fmt.Sprintf("export_%d.jsonl", exported)) - if err := doc.SaveAs(docfile); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to save genesis", "err", err) - } - exported++ - - logger.WithGroup(NodeLogName).Info("node state exported", "file", docfile) - - case rawterm.KeyN: // Next tx - logger.Info("moving forward...") - if err := dnode.MoveToNextTX(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to move forward", "err", err) - } - - case rawterm.KeyP: // Next tx - logger.Info("moving backward...") - if err := dnode.MoveToPreviousTX(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to move backward", "err", err) - } - - case rawterm.KeyCtrlC: // Exit - return nil - - default: - } - - // Reset listen for the next keypress - keyPressCh = listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) - } - } -} - -func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { - cc := make(chan rawterm.KeyPress, 1) - go func() { - defer close(cc) - key, err := rt.ReadKeyPress() - if err != nil { - logger.Error("unable to read keypress", "err", err) + // XXX: This part is a bit hacky; it mostly configures the command to + // use the local command as default, but still falls back on gnodev root + // help if asked. + var buff bytes.Buffer + cmd.SetOutput(&buff) + if err := cmd.Parse(os.Args[1:]); err != nil { + if !errors.Is(err, flag.ErrHelp) { + localcmd.Execute(context.Background(), os.Args[1:]) return } - cc <- key - }() - - return cc -} - -func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackagePath, error) { - paths := make([]gnodev.PackagePath, 0, len(args)) - - if cfg.deployKey == "" { - return nil, fmt.Errorf("default deploy key cannot be empty") - } - - defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) - if !ok { - return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) - } - - for _, arg := range args { - path, err := gnodev.ResolvePackagePathQuery(bk, arg) - if err != nil { - return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) - } - - // Assign a default creator if user haven't specified it. - if path.Creator.IsZero() { - path.Creator = defaultKey + if buff.Len() > 0 { + fmt.Fprint(stdio.Err(), buff.String()) } - paths = append(paths, path) + return } - // Add examples folder if minimal is set to false - if !cfg.minimal { - paths = append(paths, gnodev.PackagePath{ - Path: filepath.Join(cfg.root, "examples"), - Creator: defaultKey, - Deposit: nil, - }) + if err := cmd.Run(context.Background()); err != nil { + stdio.ErrPrintfln(err.Error()) } - - return paths, nil } diff --git a/contribs/gnodev/cmd/gnodev/path_manager.go b/contribs/gnodev/cmd/gnodev/path_manager.go new file mode 100644 index 00000000000..705e90fe2c4 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/path_manager.go @@ -0,0 +1,45 @@ +package main + +import ( + "sync" +) + +// pathManager manages a set of unique paths. +type pathManager struct { + paths map[string]struct{} + mu sync.RWMutex +} + +func newPathManager() *pathManager { + return &pathManager{ + paths: make(map[string]struct{}), + } +} + +// Save add one path to the PathManager. If a path already exists, it is not added again. +func (p *pathManager) Save(path string) (exist bool) { + p.mu.Lock() + defer p.mu.Unlock() + if _, exist = p.paths[path]; !exist { + p.paths[path] = struct{}{} + } + return exist +} + +func (p *pathManager) List() []string { + p.mu.RLock() + defer p.mu.RUnlock() + + paths := make([]string, 0, len(p.paths)) + for path := range p.paths { + paths = append(paths, path) + } + + return paths +} + +func (p *pathManager) Reset() { + p.mu.Lock() + defer p.mu.Unlock() + p.paths = make(map[string]struct{}) +} diff --git a/contribs/gnodev/cmd/gnodev/setup_address_book.go b/contribs/gnodev/cmd/gnodev/setup_address_book.go index a1a1c8f58ac..5d10b748a22 100644 --- a/contribs/gnodev/cmd/gnodev/setup_address_book.go +++ b/contribs/gnodev/cmd/gnodev/setup_address_book.go @@ -9,7 +9,7 @@ import ( osm "github.com/gnolang/gno/tm2/pkg/os" ) -func setupAddressBook(logger *slog.Logger, cfg *devCfg) (*address.Book, error) { +func setupAddressBook(logger *slog.Logger, cfg *AppConfig) (*address.Book, error) { book := address.NewBook() // Check for home folder @@ -40,24 +40,24 @@ func setupAddressBook(logger *slog.Logger, cfg *devCfg) (*address.Book, error) { } // Ensure that we have a default address - names, ok := book.GetByAddress(DefaultDeployerAddress) + names, ok := book.GetByAddress(defaultDeployerAddress) if ok { // Account already exist in the keybase if len(names) > 0 && names[0] != "" { - logger.Info("default address imported", "name", names[0], "addr", DefaultDeployerAddress.String()) + logger.Info("default address imported", "name", names[0], "addr", defaultDeployerAddress.String()) } else { - logger.Info("default address imported", "addr", DefaultDeployerAddress.String()) + logger.Info("default address imported", "addr", defaultDeployerAddress.String()) } return book, nil } // If the key isn't found, create a default one - creatorName := fmt.Sprintf("_default#%.6s", DefaultDeployerAddress.String()) - book.Add(DefaultDeployerAddress, creatorName) + creatorName := fmt.Sprintf("_default#%.6s", defaultDeployerAddress.String()) + book.Add(defaultDeployerAddress, creatorName) logger.Warn("default address created", "name", creatorName, - "addr", DefaultDeployerAddress.String(), + "addr", defaultDeployerAddress.String(), "mnemonic", DefaultDeployerSeed, ) diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go new file mode 100644 index 00000000000..8f10a6a5a76 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -0,0 +1,107 @@ +package main + +import ( + "fmt" + "log/slog" + "path/filepath" + "regexp" + "strings" + + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type varResolver []packages.Resolver + +func (va varResolver) String() string { + resolvers := packages.ChainedResolver(va) + return resolvers.Name() +} + +func (va *varResolver) Set(value string) error { + name, location, found := strings.Cut(value, "=") + if !found { + return fmt.Errorf("invalid resolver format %q, should be `=`", value) + } + + var res packages.Resolver + switch name { + case "remote": + rpc, err := client.NewHTTPClient(location) + if err != nil { + return fmt.Errorf("invalid resolver remote: %q", location) + } + + res = packages.NewRemoteResolver(location, rpc) + case "root": // process everything from a root directory + res = packages.NewRootResolver(location) + case "local": // process a single directory + path, ok := guessPathGnoMod(location) + if !ok { + return fmt.Errorf("unable to read module path from gno.mod in %q", location) + } + + res = packages.NewLocalResolver(path, location) + default: + return fmt.Errorf("invalid resolver name: %q", name) + } + + *va = append(*va, res) + return nil +} + +func setupPackagesResolver(logger *slog.Logger, cfg *AppConfig, dirs ...string) (packages.Resolver, []string) { + // Add root resolvers + localResolvers := make([]packages.Resolver, len(dirs)) + + var paths []string + for i, dir := range dirs { + path := guessPath(cfg, dir) + resolver := packages.NewLocalResolver(path, dir) + + if resolver.IsValid() { + logger.Info("guessing directory path", "path", path, "dir", dir) + paths = append(paths, path) // append local path + } else { + logger.Warn("no gno package found", "dir", dir) + } + + localResolvers[i] = resolver + } + + resolver := packages.ChainResolvers( + packages.ChainResolvers(localResolvers...), // Resolve local directories + packages.ChainResolvers(cfg.resolvers...), // Use user's custom resolvers + ) + + // Enrich resolver with middleware + return packages.MiddlewareResolver(resolver, + packages.CacheMiddleware(func(pkg *packages.Package) bool { + return pkg.Kind == packages.PackageKindRemote // Only cache remote package + }), + packages.FilterStdlibs, // Filter stdlib package from resolving + packages.PackageCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files + packages.LogMiddleware(logger), // Log request + ), paths +} + +func guessPathGnoMod(dir string) (path string, ok bool) { + modfile, err := gnomod.ParseAt(dir) + if err == nil { + return modfile.Module.Mod.Path, true + } + + return "", false +} + +var reInvalidChar = regexp.MustCompile(`[^\w_-]`) + +func guessPath(cfg *AppConfig, dir string) (path string) { + if path, ok := guessPathGnoMod(dir); ok { + return path + } + + rname := reInvalidChar.ReplaceAllString(filepath.Base(dir), "-") + return filepath.Join(cfg.chainDomain, "/r/dev/", rname) +} diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index eaeb89b7e95..761bdef0aef 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -9,28 +9,25 @@ import ( gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" ) // setupDevNode initializes and returns a new DevNode. -func setupDevNode( - ctx context.Context, - devCfg *devCfg, - nodeConfig *gnodev.NodeConfig, -) (*gnodev.Node, error) { +func setupDevNode(ctx context.Context, cfg *AppConfig, nodeConfig *gnodev.NodeConfig, paths ...string) (*gnodev.Node, error) { logger := nodeConfig.Logger - if devCfg.txsFile != "" { // Load txs files + if cfg.txsFile != "" { // Load txs files var err error - nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, devCfg.txsFile) + nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, cfg.txsFile) if err != nil { return nil, fmt.Errorf("unable to load transactions: %w", err) } - } else if devCfg.genesisFile != "" { // Load genesis file - state, err := extractAppStateFromGenesisFile(devCfg.genesisFile) + } else if cfg.genesisFile != "" { // Load genesis file + state, err := extractAppStateFromGenesisFile(cfg.genesisFile) if err != nil { - return nil, fmt.Errorf("unable to load genesis file %q: %w", devCfg.genesisFile, err) + return nil, fmt.Errorf("unable to load genesis file %q: %w", cfg.genesisFile, err) } // Override balances and txs @@ -43,34 +40,40 @@ func setupDevNode( nodeConfig.InitialTxs[index] = nodeTx } - logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(stateTxs)) + logger.Info("genesis file loaded", "path", cfg.genesisFile, "txs", len(stateTxs)) } - return gnodev.NewDevNode(ctx, nodeConfig) + if len(paths) > 0 { + logger.Info("packages", "paths", paths) + } else { + logger.Debug("no path(s) provided") + } + + return gnodev.NewDevNode(ctx, nodeConfig, paths...) } // setupDevNodeConfig creates and returns a new dev.NodeConfig. func setupDevNodeConfig( - cfg *devCfg, + cfg *AppConfig, logger *slog.Logger, emitter emitter.Emitter, balances gnoland.Balances, - pkgspath []gnodev.PackagePath, + loader packages.Loader, ) *gnodev.NodeConfig { config := gnodev.DefaultNodeConfig(cfg.root, cfg.chainDomain) + config.Loader = loader config.Logger = logger config.Emitter = emitter config.BalancesList = balances.List() - config.PackagesPathList = pkgspath - config.TMConfig.RPC.ListenAddress = resolveUnixOrTCPAddr(cfg.nodeRPCListenerAddr) + config.TMConfig.RPC.ListenAddress = cfg.nodeRPCListenerAddr config.NoReplay = cfg.noReplay config.MaxGasPerBlock = cfg.maxGas config.ChainID = cfg.chainId // other listeners - config.TMConfig.P2P.ListenAddress = defaultDevOptions.nodeP2PListenerAddr - config.TMConfig.ProxyApp = defaultDevOptions.nodeProxyAppListenerAddr + config.TMConfig.P2P.ListenAddress = defaultLocalAppConfig.nodeP2PListenerAddr + config.TMConfig.ProxyApp = defaultLocalAppConfig.nodeProxyAppListenerAddr return config } @@ -89,21 +92,20 @@ func extractAppStateFromGenesisFile(path string) (*gnoland.GnoGenesisState, erro return &state, nil } -func resolveUnixOrTCPAddr(in string) (out string) { +func resolveUnixOrTCPAddr(in string) (addr net.Addr) { var err error - var addr net.Addr if strings.HasPrefix(in, "unix://") { in = strings.TrimPrefix(in, "unix://") - if addr, err := net.ResolveUnixAddr("unix", in); err == nil { - return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + if addr, err = net.ResolveUnixAddr("unix", in); err == nil { + return addr } err = fmt.Errorf("unable to resolve unix address `unix://%s`: %w", in, err) } else { // don't bother to checking prefix in = strings.TrimPrefix(in, "tcp://") if addr, err = net.ResolveTCPAddr("tcp", in); err == nil { - return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + return addr } err = fmt.Errorf("unable to resolve tcp address `tcp://%s`: %w", in, err) diff --git a/contribs/gnodev/cmd/gnodev/setup_term.go b/contribs/gnodev/cmd/gnodev/setup_term.go index 1f8f3046969..fb8b5593abf 100644 --- a/contribs/gnodev/cmd/gnodev/setup_term.go +++ b/contribs/gnodev/cmd/gnodev/setup_term.go @@ -7,10 +7,10 @@ import ( var noopRestore = func() error { return nil } -func setupRawTerm(cfg *devCfg, io commands.IO) (*rawterm.RawTerm, func() error, error) { +func setupRawTerm(cfg *AppConfig, io commands.IO) (*rawterm.RawTerm, func() error, error) { rt := rawterm.NewRawTerm() restore := noopRestore - if !cfg.serverMode { + if cfg.interactive { var err error restore, err = rt.Init() if err != nil { diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index e509768d2a1..09df8ce009c 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -5,24 +5,23 @@ import ( "log/slog" "net/http" - gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/gno.land/pkg/gnoweb" ) // setupGnowebServer initializes and starts the Gnoweb server. -func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) (http.Handler, error) { +func setupGnoWebServer(logger *slog.Logger, cfg *AppConfig, remoteAddr string) (http.Handler, error) { if cfg.noWeb { return http.HandlerFunc(http.NotFound), nil } - remote := dnode.GetRemoteAddress() - appcfg := gnoweb.NewDefaultAppConfig() appcfg.UnsafeHTML = cfg.webHTML - appcfg.NodeRemote = remote + appcfg.NodeRemote = remoteAddr appcfg.ChainID = cfg.chainId if cfg.webRemoteHelperAddr != "" { appcfg.RemoteHelp = cfg.webRemoteHelperAddr + } else { + appcfg.RemoteHelp = remoteAddr } router, err := gnoweb.NewRouter(logger, appcfg) @@ -30,5 +29,11 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) (ht return nil, fmt.Errorf("unable to create router app: %w", err) } + logger.Debug("gnoweb router created", + "remote", appcfg.NodeRemote, + "helper_remote", appcfg.RemoteHelp, + "html", appcfg.UnsafeHTML, + "chain_id", cfg.chainId, + ) return router, nil } diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 0ad16ba9bb3..9aea08c4a30 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -18,6 +18,7 @@ require ( github.com/gnolang/gno v0.0.0-00010101000000-000000000000 github.com/gorilla/websocket v1.5.3 github.com/lrstanley/bubblezone v0.0.0-20240624011428-67235275f80c + github.com/mattn/go-isatty v0.0.20 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.15.2 github.com/sahilm/fuzzy v0.1.1 @@ -62,7 +63,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect diff --git a/contribs/gnodev/internal/mock/server_emitter.go b/contribs/gnodev/internal/mock/emitter/server_emitter.go similarity index 100% rename from contribs/gnodev/internal/mock/server_emitter.go rename to contribs/gnodev/internal/mock/emitter/server_emitter.go diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 12a88490515..5fa9dc4e4d4 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -13,10 +13,12 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" - "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/amino" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -32,18 +34,52 @@ import ( ) type NodeConfig struct { - Logger *slog.Logger - DefaultDeployer crypto.Address - BalancesList []gnoland.Balance - PackagesPathList []PackagePath - Emitter emitter.Emitter - InitialTxs []gnoland.TxWithMetadata - TMConfig *tmcfg.Config + // Logger is used for logging node activities. It can be set to a custom logger or a noop logger for + // silent operation. + Logger *slog.Logger + + // Loader is responsible for loading packages. It abstracts the mechanism for retrieving and managing + // package data. + Loader packages.Loader + + // DefaultCreator specifies the default address used for creating packages and transactions. + DefaultCreator crypto.Address + + // DefaultDeposit is the default amount of coins deposited when creating a package. + DefaultDeposit std.Coins + + // BalancesList defines the initial balance of accounts in the genesis state. + BalancesList []gnoland.Balance + + // PackagesModifier allows modifications to be applied to packages during initialization. + PackagesModifier []QueryPath + + // Emitter is used to emit events for various node operations. It can be set to a noop emitter if no + // event emission is required. + Emitter emitter.Emitter + + // InitialTxs contains the transactions that are included in the genesis state. + InitialTxs []gnoland.TxWithMetadata + + // TMConfig holds the Tendermint configuration settings. + TMConfig *tmcfg.Config + + // SkipFailingGenesisTxs indicates whether to skip failing transactions during the genesis + // initialization. SkipFailingGenesisTxs bool - NoReplay bool - MaxGasPerBlock int64 - ChainID string - ChainDomain string + + // NoReplay, if set to true, prevents replaying of transactions from the block store during node + // initialization. + NoReplay bool + + // MaxGasPerBlock sets the maximum amount of gas that can be used in a single block. + MaxGasPerBlock int64 + + // ChainID is the unique identifier for the blockchain. + ChainID string + + // ChainDomain specifies the domain name associated with the blockchain network. + ChainDomain string } func DefaultNodeConfig(rootdir, domain string) *NodeConfig { @@ -60,10 +96,15 @@ func DefaultNodeConfig(rootdir, domain string) *NodeConfig { }, } + exampleFolder := filepath.Join(gnoenv.RootDir(), "example") // XXX: we should avoid having to hardcoding this here + defaultLoader := packages.NewLoader(packages.NewRootResolver(exampleFolder)) + return &NodeConfig{ Logger: log.NewNoopLogger(), Emitter: &emitter.NoopServer{}, - DefaultDeployer: defaultDeployer, + Loader: defaultLoader, + DefaultCreator: defaultDeployer, + DefaultDeposit: nil, BalancesList: balances, ChainID: tmc.ChainID(), ChainDomain: domain, @@ -78,11 +119,14 @@ type Node struct { *node.Node muNode sync.RWMutex - config *NodeConfig - emitter emitter.Emitter - client client.Client - logger *slog.Logger - pkgs PackagesMap // path -> pkg + config *NodeConfig + emitter emitter.Emitter + client client.Client + logger *slog.Logger + loader packages.Loader + pkgs []packages.Package + pkgsModifier map[string]QueryPath // path -> QueryPath + paths []string // keep track of number of loaded package to be able to skip them on restore loadedPackages int @@ -97,36 +141,30 @@ type Node struct { var DefaultFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) -func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { - mpkgs, err := NewPackagesMap(cfg.PackagesPathList) - if err != nil { - return nil, fmt.Errorf("unable map pkgs list: %w", err) - } - +func NewDevNode(ctx context.Context, cfg *NodeConfig, pkgpaths ...string) (*Node, error) { startTime := time.Now() - pkgsTxs, err := mpkgs.Load(DefaultFee, startTime) - if err != nil { - return nil, fmt.Errorf("unable to load genesis packages: %w", err) + + pkgsModifier := make(map[string]QueryPath, len(cfg.PackagesModifier)) + for _, qpath := range cfg.PackagesModifier { + pkgsModifier[qpath.Path] = qpath } - cfg.Logger.Info("pkgs loaded", "path", cfg.PackagesPathList) devnode := &Node{ + loader: cfg.Loader, config: cfg, client: client.NewLocal(), emitter: cfg.Emitter, - pkgs: mpkgs, logger: cfg.Logger, - loadedPackages: len(pkgsTxs), startTime: startTime, state: cfg.InitialTxs, initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), + paths: pkgpaths, + pkgsModifier: pkgsModifier, } - genesis := gnoland.DefaultGenState() - genesis.Balances = cfg.BalancesList - genesis.Txs = append(pkgsTxs, cfg.InitialTxs...) - if err := devnode.rebuildNode(ctx, genesis); err != nil { + // XXX: MOVE THIS, passing context here can be confusing + if err := devnode.Reset(ctx); err != nil { return nil, fmt.Errorf("unable to initialize the node: %w", err) } @@ -140,11 +178,11 @@ func (n *Node) Close() error { return n.Node.Stop() } -func (n *Node) ListPkgs() []gnomod.Pkg { +func (n *Node) ListPkgs() []packages.Package { n.muNode.RLock() defer n.muNode.RUnlock() - return n.pkgs.toList() + return n.pkgs } func (n *Node) Client() client.Client { @@ -158,8 +196,38 @@ func (n *Node) GetRemoteAddress() string { return n.Node.Config().RPC.ListenAddress } +// AddPackagePaths to load +func (n *Node) AddPackagePaths(paths ...string) { + n.muNode.Lock() + defer n.muNode.Unlock() + + n.paths = append(n.paths, paths...) +} + +func (n *Node) SetPackagePaths(paths ...string) { + n.muNode.Lock() + defer n.muNode.Unlock() + + n.paths = paths +} + +// HasPackageLoaded returns true if the specified package has already been loaded. +// NOTE: This only checks if the package was loaded at the genesis level. +func (n *Node) HasPackageLoaded(path string) bool { + n.muNode.RLock() + defer n.muNode.RUnlock() + + for _, pkg := range n.pkgs { + if pkg.MemPackage.Path == path { + return true + } + } + + return false +} + // GetBlockTransactions returns the transactions contained -// within the specified block, if any +// within the specified block, if any. func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { n.muNode.RLock() defer n.muNode.RUnlock() @@ -168,7 +236,7 @@ func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, } // GetBlockTransactions returns the transactions contained -// within the specified block, if any +// within the specified block, if any. func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { int64BlockNum := int64(blockNum) b, err := n.client.Block(&int64BlockNum) @@ -196,8 +264,8 @@ func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, } // GetBlockTransactions returns the transactions contained -// within the specified block, if any -// GetLatestBlockNumber returns the latest block height from the chain +// within the specified block, if any. +// GetLatestBlockNumber returns the latest block height from the chain. func (n *Node) GetLatestBlockNumber() (uint64, error) { n.muNode.RLock() defer n.muNode.RUnlock() @@ -209,82 +277,25 @@ func (n *Node) getLatestBlockNumber() uint64 { return uint64(n.Node.BlockStore().Height()) } -// UpdatePackages updates the currently known packages. It will be taken into -// consideration in the next reload of the node. -func (n *Node) UpdatePackages(paths ...string) error { - n.muNode.Lock() - defer n.muNode.Unlock() - - return n.updatePackages(paths...) -} - -func (n *Node) updatePackages(paths ...string) error { - var pkgsUpdated int - for _, path := range paths { - abspath, err := filepath.Abs(path) - if err != nil { - return fmt.Errorf("unable to resolve abs path of %q: %w", path, err) - } - - // Check if we already know the path (or its parent) and set - // associated deployer and deposit - deployer := n.config.DefaultDeployer - var deposit std.Coins - for _, ppath := range n.config.PackagesPathList { - if !strings.HasPrefix(abspath, ppath.Path) { - continue - } - - deployer = ppath.Creator - deposit = ppath.Deposit - } - - // List all packages from target path - pkgslist, err := gnomod.ListPkgs(abspath) - if err != nil { - return fmt.Errorf("failed to list gno packages for %q: %w", path, err) - } - - // Update or add package in the current known list. - for _, pkg := range pkgslist { - n.pkgs[pkg.Dir] = Package{ - Pkg: pkg, - Creator: deployer, - Deposit: deposit, - } - - n.logger.Debug("pkgs update", "name", pkg.Name, "path", pkg.Dir) - } - - pkgsUpdated += len(pkgslist) - } - - n.logger.Info(fmt.Sprintf("updated %d packages", pkgsUpdated)) - return nil -} - // Reset stops the node, if running, and reloads it with a new genesis state, // effectively ignoring the current state. func (n *Node) Reset(ctx context.Context) error { n.muNode.Lock() defer n.muNode.Unlock() - // Stop the node if it's currently running. - if err := n.stopIfRunning(); err != nil { - return fmt.Errorf("unable to stop the node: %w", err) - } - // Reset starting time startTime := time.Now() // Generate a new genesis state based on the current packages - pkgsTxs, err := n.pkgs.Load(DefaultFee, startTime) + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } // Append initialTxs + pkgsTxs := n.generateTxs(DefaultFee, pkgs) txs := append(pkgsTxs, n.initialState...) + genesis := gnoland.DefaultGenState() genesis.Balances = n.config.BalancesList genesis.Txs = txs @@ -295,6 +306,7 @@ func (n *Node) Reset(ctx context.Context) error { return fmt.Errorf("unable to initialize a new node: %w", err) } + n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) n.currentStateIndex = len(n.initialState) n.startTime = startTime @@ -308,16 +320,6 @@ func (n *Node) ReloadAll(ctx context.Context) error { n.muNode.Lock() defer n.muNode.Unlock() - pkgs := n.pkgs.toList() - paths := make([]string, len(pkgs)) - for i, pkg := range pkgs { - paths[i] = pkg.Dir - } - - if err := n.updatePackages(paths...); err != nil { - return fmt.Errorf("unable to reload packages: %w", err) - } - return n.rebuildNodeFromState(ctx) } @@ -386,10 +388,51 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata state = append(state, txs...) } - // override current state return state, nil } +func (n *Node) generateTxs(fee std.Fee, pkgs []packages.Package) []gnoland.TxWithMetadata { + metatxs := make([]gnoland.TxWithMetadata, 0, len(pkgs)) + for _, pkg := range pkgs { + msg := vm.MsgAddPackage{ + Creator: n.config.DefaultCreator, + Deposit: n.config.DefaultDeposit, + Package: &pkg.MemPackage, + } + + if m, ok := n.pkgsModifier[pkg.Path]; ok { + if !m.Creator.IsZero() { + msg.Creator = m.Creator + } + + if m.Deposit != nil { + msg.Deposit = m.Deposit + } + + n.logger.Debug("applying pkgs modifier", + "path", pkg.Path, + "creator", msg.Creator, + "deposit", msg.Deposit, + ) + } + + // Create transaction + tx := std.Tx{Fee: fee, Msgs: []std.Msg{msg}} + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + + // Wrap it with metadata + metatx := gnoland.TxWithMetadata{ + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: n.startTime.Unix(), + }, + } + metatxs = append(metatxs, metatx) + } + + return metatxs +} + func (n *Node) stopIfRunning() error { if n.Node != nil && n.Node.IsRunning() { if err := n.Node.Stop(); err != nil { @@ -401,17 +444,20 @@ func (n *Node) stopIfRunning() error { } func (n *Node) rebuildNodeFromState(ctx context.Context) error { + start := time.Now() + if n.config.NoReplay { // If NoReplay is true, simply reset the node to its initial state n.logger.Warn("replay disabled") - txs, err := n.pkgs.Load(DefaultFee, n.startTime) + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } + genesis := gnoland.DefaultGenState() genesis.Balances = n.config.BalancesList - genesis.Txs = txs + genesis.Txs = n.generateTxs(DefaultFee, pkgs) return n.rebuildNode(ctx, genesis) } @@ -421,7 +467,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -429,15 +475,24 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // Create genesis with loaded pkgs + previous state genesis := gnoland.DefaultGenState() genesis.Balances = n.config.BalancesList + + // Generate txs + pkgsTxs := n.generateTxs(DefaultFee, pkgs) genesis.Txs = append(pkgsTxs, state...) // Reset the node with the new genesis state. err = n.rebuildNode(ctx, genesis) - n.logger.Info("reload done", "pkgs", len(pkgsTxs), "state applied", len(state)) + n.logger.Info("reload done", + "pkgs", len(pkgsTxs), + "state applied", len(state), + "took", time.Since(start), + ) // Update node infos + n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) + // Emit reload event n.emitter.Emit(&events.Reload{}) return nil } @@ -534,13 +589,23 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { if !res.IsErr() { + for _, msg := range tx.Msgs { + if addpkg, ok := msg.(vm.MsgAddPackage); ok && addpkg.Package != nil { + n.logger.Debug("add package", + "path", addpkg.Package.Path, + "files", len(addpkg.Package.Files), + "creator", addpkg.Creator.String(), + ) + } + } + return } // XXX: for now, this is only way to catch the error before, after, found := strings.Cut(res.Log, "\n") if !found { - n.logger.Error("unable to send tx", "err", res.Error, "log", res.Log) + n.logger.Error("unable to send tx", "log", res.Log) return } diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 3f996bc7716..557565ea0b1 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -84,14 +84,10 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) - if err != nil { - return fmt.Errorf("unable to load pkgs: %w", err) - } - - newState := n.state[:newIndex] + pkgsTxs := n.generateTxs(DefaultFee, n.pkgs) // Create genesis with loaded pkgs + previous state + newState := n.state[:newIndex] genesis := gnoland.DefaultGenState() genesis.Balances = n.config.BalancesList genesis.Txs = append(pkgsTxs, newState...) diff --git a/contribs/gnodev/pkg/dev/node_state_test.go b/contribs/gnodev/pkg/dev/node_state_test.go index efaeb979693..32800fd0db6 100644 --- a/contribs/gnodev/pkg/dev/node_state_test.go +++ b/contribs/gnodev/pkg/dev/node_state_test.go @@ -6,10 +6,11 @@ import ( "testing" "time" - emitter "github.com/gnolang/gno/contribs/gnodev/internal/mock" + mock "github.com/gnolang/gno/contribs/gnodev/internal/mock/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -136,28 +137,29 @@ func TestExportState(t *testing.T) { }) } -func testingCounterRealm(t *testing.T, inc int) (*Node, *emitter.ServerEmitter) { +func testingCounterRealm(t *testing.T, inc int) (*Node, *mock.ServerEmitter) { t.Helper() - const ( - // foo package - counterGnoMod = "module gno.land/r/dev/counter\n" - counterFile = `package counter + const counterFile = ` +package counter + import "strconv" var value int = 0 func Inc(v int) { value += v } // method to increment value func Render(_ string) string { return strconv.Itoa(value) } ` - ) - // Generate package counter - counterPkg := generateTestingPackage(t, - "gno.mod", counterGnoMod, - "foo.gno", counterFile) + counterPkg := gnovm.MemPackage{ + Name: "counter", + Path: "gno.land/r/dev/counter", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: counterFile}, + }, + } // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, counterPkg) + node, emitter := newTestingDevNode(t, &counterPkg) assert.Len(t, node.ListPkgs(), 1) // Test rendering diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 38fab0a3360..89fd419a9ae 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -3,22 +3,20 @@ package dev import ( "context" "encoding/json" - "os" - "path/filepath" "testing" "time" - mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" - + mock "github.com/gnolang/gno/contribs/gnodev/internal/mock/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gno.land/pkg/gnoclient" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" tm2events "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" @@ -26,10 +24,6 @@ import ( "github.com/stretchr/testify/require" ) -// XXX: We should probably use txtar to test this package. - -var nodeTestingAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - // TestNewNode_NoPackages tests the NewDevNode method with no package. func TestNewNode_NoPackages(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) @@ -49,32 +43,35 @@ func TestNewNode_NoPackages(t *testing.T) { } // TestNewNode_WithPackage tests the NewDevNode with a single package. -func TestNewNode_WithPackage(t *testing.T) { +func TestNewNode_WithLoader(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - const ( - // foobar package - testGnoMod = "module gno.land/r/dev/foobar\n" - testFile = `package foobar + pkg := gnovm.MemPackage{ + Name: "foobar", + Path: "gno.land/r/dev/foobar", + Files: []*gnovm.MemFile{ + { + Name: "foobar.gno", + Body: `package foobar func Render(_ string) string { return "foo" } -` - ) +`, + }, + }, + } - // Generate package - pkgpath := generateTestingPackage(t, "gno.mod", testGnoMod, "foobar.gno", testFile) logger := log.NewTestingLogger(t) - // Call NewDevNode with no package should work cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") - cfg.PackagesPathList = []PackagePath{pkgpath} + cfg.Loader = packages.NewLoader(packages.NewMockResolver(&pkg)) cfg.Logger = logger - node, err := NewDevNode(ctx, cfg) + + node, err := NewDevNode(ctx, cfg, pkg.Path) require.NoError(t, err) assert.Len(t, node.ListPkgs(), 1) // Test rendering - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foobar") + render, err := testingRenderRealm(t, node, pkg.Path) require.NoError(t, err) assert.Equal(t, render, "foo") @@ -83,24 +80,37 @@ func Render(_ string) string { return "foo" } func TestNodeAddPackage(t *testing.T) { // Setup a Node instance - const ( - // foo package - fooGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo + fooPkg := gnovm.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo func Render(_ string) string { return "foo" } -` - // bar package - barGnoMod = "module gno.land/r/dev/bar\n" - barFile = `package bar +`, + }, + }, + } + + barPkg := gnovm.MemPackage{ + Name: "bar", + Path: "gno.land/r/dev/bar", + Files: []*gnovm.MemFile{ + { + Name: "bar.gno", + Body: `package bar func Render(_ string) string { return "bar" } -` - ) +`, + }, + }, + } // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", fooGnoMod, "foo.gno", fooFile) + cfg := newTestingNodeConfig(&fooPkg, &barPkg) // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) + node, emitter := newTestingDevNodeWithConfig(t, cfg, fooPkg.Path) assert.Len(t, node.ListPkgs(), 1) // Test render @@ -108,54 +118,60 @@ func Render(_ string) string { return "bar" } require.NoError(t, err) require.Equal(t, render, "foo") - // Generate package bar - barpkg := generateTestingPackage(t, "gno.mod", barGnoMod, "bar.gno", barFile) - err = node.UpdatePackages(barpkg.Path) - require.NoError(t, err) - assert.Len(t, node.ListPkgs(), 2) - // Render should fail as the node hasn't reloaded render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") require.Error(t, err) + // Add bar package + node.AddPackagePaths(barPkg.Path) + err = node.Reload(context.Background()) require.NoError(t, err) assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) // After a reload, render should succeed - render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") + render, err = testingRenderRealm(t, node, barPkg.Path) require.NoError(t, err) require.Equal(t, render, "bar") } func TestNodeUpdatePackage(t *testing.T) { - // Setup a Node instance - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foobar\n" - fooFile = `package foobar + foorbarPkg := gnovm.MemPackage{ + Name: "foobar", + Path: "gno.land/r/dev/foobar", + } + + fooFiles := []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foobar func Render(_ string) string { return "foo" } -` - barFile = `package foobar +`, + }, + } + + barFiles := []*gnovm.MemFile{ + { + Name: "bar.gno", + Body: `package foobar func Render(_ string) string { return "bar" } -` - ) +`, + }, + } - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + // Update foobar content with bar content + foorbarPkg.Files = fooFiles - // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) + node, emitter := newTestingDevNode(t, &foorbarPkg) assert.Len(t, node.ListPkgs(), 1) // Test that render is correct - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foobar") + render, err := testingRenderRealm(t, node, foorbarPkg.Path) require.NoError(t, err) require.Equal(t, render, "foo") - // Override `foo.gno` file with bar content - err = os.WriteFile(filepath.Join(foopkg.Path, "foo.gno"), []byte(barFile), 0o700) - require.NoError(t, err) + // Update foobar content with bar content + foorbarPkg.Files = barFiles err = node.Reload(context.Background()) require.NoError(t, err) @@ -164,7 +180,7 @@ func Render(_ string) string { return "bar" } assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) // After a reload, render should succeed - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foobar") + render, err = testingRenderRealm(t, node, foorbarPkg.Path) require.NoError(t, err) require.Equal(t, render, "bar") @@ -172,31 +188,33 @@ func Render(_ string) string { return "bar" } } func TestNodeReset(t *testing.T) { - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo + fooPkg := gnovm.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo var str string = "foo" func UpdateStr(newStr string) { str = newStr } // method to update 'str' variable func Render(_ string) string { return str } -` - ) - - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) +`, + }, + }, + } // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) + node, emitter := newTestingDevNode(t, &fooPkg) assert.Len(t, node.ListPkgs(), 1) // Test rendering - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err := testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) require.Equal(t, render, "foo") // Call `UpdateStr` to update `str` value with "bar" msg := vm.MsgCall{ - PkgPath: "gno.land/r/dev/foo", + PkgPath: fooPkg.Path, Func: "UpdateStr", Args: []string{"bar"}, Send: nil, @@ -208,7 +226,7 @@ func Render(_ string) string { return str } assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) // Check for correct render update - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err = testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) require.Equal(t, render, "bar") @@ -218,7 +236,7 @@ func Render(_ string) string { return str } assert.Equal(t, emitter.NextEvent().Type(), events.EvtReset) // Test rendering should return initial `str` value - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err = testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) require.Equal(t, render, "foo") @@ -226,10 +244,9 @@ func Render(_ string) string { return str } } func TestTxTimestampRecover(t *testing.T) { - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo + const fooFile = ` +package foo + import ( "strconv" "strings" @@ -259,12 +276,35 @@ func Render(_ string) string { return strs.String() } ` - ) + fooPkg := gnovm.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: fooFile, + }, + }, + } // Add a hard deadline of 20 seconds to avoid potential deadlock and fail early ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() + // XXX(gfanton): Setting this to `false` somehow makes the time block + // drift from the time spanned by the VM. + cfg := newTestingNodeConfig(&fooPkg) + cfg.TMConfig.Consensus.SkipTimeoutCommit = false + cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond + cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond + cfg.TMConfig.Consensus.CreateEmptyBlocks = true + + node, emitter := newTestingDevNodeWithConfig(t, cfg, fooPkg.Path) + + render, err := testingRenderRealm(t, node, fooPkg.Path) + require.NoError(t, err) + require.NotEmpty(t, render) + parseJSONTimesList := func(t *testing.T, render string) []time.Time { t.Helper() @@ -282,21 +322,6 @@ func Render(_ string) string { return times } - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) - - // Call NewDevNode with no package should work - cfg := createDefaultTestingNodeConfig(foopkg) - - // XXX(gfanton): Setting this to `false` somehow makes the time block - // drift from the time spanned by the VM. - cfg.TMConfig.Consensus.SkipTimeoutCommit = false - cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond - cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond - cfg.TMConfig.Consensus.CreateEmptyBlocks = true - - node, emitter := newTestingDevNodeWithConfig(t, cfg) - // We need to make sure that blocks are separated by at least 1 second // (minimal time between blocks). We can ensure this by listening for // new blocks and comparing timestamps @@ -329,7 +354,7 @@ func Render(_ string) string { // Span multiple time for i := 0; i < nevents; i++ { - t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp) + t.Logf("waiting for a block greater than height(%d) and unix(%d)", refHeight, refTimestamp) for { var block types.EventNewBlock select { @@ -357,7 +382,7 @@ func Render(_ string) string { // Span a new time msg := vm.MsgCall{ - PkgPath: "gno.land/r/dev/foo", + PkgPath: fooPkg.Path, Func: "SpanTime", } @@ -373,7 +398,7 @@ func Render(_ string) string { } // Render JSON times list - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err = testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) // Parse times list @@ -396,12 +421,12 @@ func Render(_ string) string { assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) // Fetch time list again from render - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err = testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) timesList2 := parseJSONTimesList(t, render) - // Times list should be identical from the orignal list + // Times list should be identical from the original list require.Len(t, timesList2, len(timesList1)) for i := 0; i < len(timesList1); i++ { t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() @@ -452,42 +477,32 @@ func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types return cli.Call(txcfg, vmMsgs...) } -func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { - t.Helper() - workdir := t.TempDir() +func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig { + var loader packages.BaseLoader + gnoroot := gnoenv.RootDir() - if len(nameFile)%2 != 0 { - require.FailNow(t, "Generate testing packages require paired arguments.") - } - - for i := 0; i < len(nameFile); i += 2 { - name := nameFile[i] - content := nameFile[i+1] - - err := os.WriteFile(filepath.Join(workdir, name), []byte(content), 0o700) - require.NoError(t, err) - } - - return PackagePath{ - Path: workdir, - Creator: nodeTestingAddress, - } -} - -func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { + loader.Resolver = packages.MiddlewareResolver( + packages.NewMockResolver(pkgs...), + packages.FilterStdlibs) cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") - cfg.PackagesPathList = pkgslist + cfg.TMConfig = integration.DefaultTestingTMConfig(gnoroot) + cfg.Loader = &loader return cfg } -func newTestingDevNode(t *testing.T, pkgslist ...PackagePath) (*Node, *mock.ServerEmitter) { +func newTestingDevNode(t *testing.T, pkgs ...*gnovm.MemPackage) (*Node, *mock.ServerEmitter) { t.Helper() - cfg := createDefaultTestingNodeConfig(pkgslist...) - return newTestingDevNodeWithConfig(t, cfg) + cfg := newTestingNodeConfig(pkgs...) + paths := make([]string, len(pkgs)) + for i, pkg := range pkgs { + paths[i] = pkg.Path + } + + return newTestingDevNodeWithConfig(t, cfg, paths...) } -func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) { +func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig, pkgpaths ...string) (*Node, *mock.ServerEmitter) { t.Helper() ctx, cancel := context.WithCancel(context.Background()) @@ -497,9 +512,9 @@ func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.Se cfg.Emitter = emitter cfg.Logger = logger - node, err := NewDevNode(ctx, cfg) + node, err := NewDevNode(ctx, cfg, pkgpaths...) require.NoError(t, err) - assert.Len(t, node.ListPkgs(), len(cfg.PackagesPathList)) + require.Equal(t, emitter.NextEvent().Type(), events.EvtReset) t.Cleanup(func() { node.Close() diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go deleted file mode 100644 index 62c1907b8c9..00000000000 --- a/contribs/gnodev/pkg/dev/packages.go +++ /dev/null @@ -1,170 +0,0 @@ -package dev - -import ( - "errors" - "fmt" - "net/url" - "path/filepath" - "time" - - "github.com/gnolang/gno/contribs/gnodev/pkg/address" - "github.com/gnolang/gno/gno.land/pkg/gnoland" - vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/pkg/gnomod" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type PackagePath struct { - Path string - Creator crypto.Address - Deposit std.Coins -} - -func ResolvePackagePathQuery(bk *address.Book, path string) (PackagePath, error) { - var ppath PackagePath - - upath, err := url.Parse(path) - if err != nil { - return ppath, fmt.Errorf("malformed path/query: %w", err) - } - ppath.Path = filepath.Clean(upath.Path) - - // Check for creator option - creator := upath.Query().Get("creator") - if creator != "" { - address, err := crypto.AddressFromBech32(creator) - if err != nil { - var ok bool - address, ok = bk.GetByName(creator) - if !ok { - return ppath, fmt.Errorf("invalid name or address for creator %q", creator) - } - } - - ppath.Creator = address - } - - // Check for deposit option - deposit := upath.Query().Get("deposit") - if deposit != "" { - coins, err := std.ParseCoins(deposit) - if err != nil { - return ppath, fmt.Errorf( - "unable to parse deposit amount %q (should be in the form xxxugnot): %w", deposit, err, - ) - } - - ppath.Deposit = coins - } - - return ppath, nil -} - -type Package struct { - gnomod.Pkg - Creator crypto.Address - Deposit std.Coins -} - -type PackagesMap map[string]Package - -var ( - ErrEmptyCreatorPackage = errors.New("no creator specified for package") - ErrEmptyDepositPackage = errors.New("no deposit specified for package") -) - -func NewPackagesMap(ppaths []PackagePath) (PackagesMap, error) { - pkgs := make(map[string]Package) - for _, ppath := range ppaths { - if ppath.Creator.IsZero() { - return nil, fmt.Errorf("unable to load package %q: %w", ppath.Path, ErrEmptyCreatorPackage) - } - - abspath, err := filepath.Abs(ppath.Path) - if err != nil { - return nil, fmt.Errorf("unable to guess absolute path for %q: %w", ppath.Path, err) - } - - // list all packages from target path - pkgslist, err := gnomod.ListPkgs(abspath) - if err != nil { - return nil, fmt.Errorf("listing gno packages: %w", err) - } - - for _, pkg := range pkgslist { - if pkg.Dir == "" { - continue - } - - if _, ok := pkgs[pkg.Dir]; ok { - continue // skip - } - pkgs[pkg.Dir] = Package{ - Pkg: pkg, - Creator: ppath.Creator, - Deposit: ppath.Deposit, - } - } - } - - return pkgs, nil -} - -func (pm PackagesMap) toList() gnomod.PkgList { - list := make([]gnomod.Pkg, 0, len(pm)) - for _, pkg := range pm { - list = append(list, pkg.Pkg) - } - return list -} - -func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetadata, error) { - pkgs := pm.toList() - - sorted, err := pkgs.Sort() - if err != nil { - return nil, fmt.Errorf("unable to sort pkgs: %w", err) - } - - nonDraft := sorted.GetNonDraftPkgs() - - metatxs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) - for _, modPkg := range nonDraft { - pkg := pm[modPkg.Dir] - if pkg.Creator.IsZero() { - return nil, fmt.Errorf("no creator set for %q", pkg.Dir) - } - - // Open files in directory as MemPackage. - memPkg := gno.MustReadMemPackage(modPkg.Dir, modPkg.Name) - if err := memPkg.Validate(); err != nil { - return nil, fmt.Errorf("invalid package: %w", err) - } - - // Create transaction - tx := std.Tx{ - Fee: fee, - Msgs: []std.Msg{ - vmm.MsgAddPackage{ - Creator: pkg.Creator, - Deposit: pkg.Deposit, - Package: memPkg, - }, - }, - } - - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - metatx := gnoland.TxWithMetadata{ - Tx: tx, - Metadata: &gnoland.GnoTxMetadata{ - Timestamp: start.Unix(), - }, - } - - metatxs = append(metatxs, metatx) - } - - return metatxs, nil -} diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go deleted file mode 100644 index 151a89a7815..00000000000 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package dev - -import ( - "testing" - - "github.com/gnolang/gno/contribs/gnodev/pkg/address" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestResolvePackagePathQuery(t *testing.T) { - t.Parallel() - - var ( - testingName = "testAccount" - testingAddress = crypto.MustAddressFromString("g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na") - ) - - book := address.NewBook() - book.Add(testingAddress, testingName) - - cases := []struct { - Path string - ExpectedPackagePath PackagePath - ShouldFail bool - }{ - { - Path: ".", - ExpectedPackagePath: PackagePath{ - Path: ".", - }, - }, - { - Path: "/simple/path", - ExpectedPackagePath: PackagePath{ - Path: "/simple/path", - }, - }, - { - Path: "/ambiguo/u//s/path///", - ExpectedPackagePath: PackagePath{ - Path: "/ambiguo/u/s/path", - }, - }, - { - Path: "/path/with/creator?creator=testAccount", - ExpectedPackagePath: PackagePath{ - Path: "/path/with/creator", - Creator: testingAddress, - }, - }, - { - Path: "/path/with/deposit?deposit=" + ugnot.ValueString(100), - ExpectedPackagePath: PackagePath{ - Path: "/path/with/deposit", - Deposit: std.MustParseCoins(ugnot.ValueString(100)), - }, - }, - { - Path: ".?creator=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=" + ugnot.ValueString(100), - ExpectedPackagePath: PackagePath{ - Path: ".", - Creator: testingAddress, - Deposit: std.MustParseCoins(ugnot.ValueString(100)), - }, - }, - - // errors cases - { - Path: "/invalid/account?creator=UnknownAccount", - ShouldFail: true, - }, - { - Path: "/invalid/address?creator=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", - ShouldFail: true, - }, - { - Path: "/invalid/deposit?deposit=abcd", - ShouldFail: true, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.Path, func(t *testing.T) { - t.Parallel() - - result, err := ResolvePackagePathQuery(book, tc.Path) - if tc.ShouldFail { - assert.Error(t, err) - return - } - require.NoError(t, err) - - assert.Equal(t, tc.ExpectedPackagePath.Path, result.Path) - assert.Equal(t, tc.ExpectedPackagePath.Creator, result.Creator) - assert.Equal(t, tc.ExpectedPackagePath.Deposit.String(), result.Deposit.String()) - }) - } -} diff --git a/contribs/gnodev/pkg/dev/query_path.go b/contribs/gnodev/pkg/dev/query_path.go new file mode 100644 index 00000000000..e899d8212e4 --- /dev/null +++ b/contribs/gnodev/pkg/dev/query_path.go @@ -0,0 +1,58 @@ +package dev + +import ( + "fmt" + "net/url" + "path/filepath" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type QueryPath struct { + Path string + Creator crypto.Address + Deposit std.Coins +} + +func ResolveQueryPath(bk *address.Book, query string) (QueryPath, error) { + var qpath QueryPath + + upath, err := url.Parse(query) + if err != nil { + return qpath, fmt.Errorf("malformed path/query: %w", err) + } + + qpath.Path = filepath.Clean(upath.Path) + + // Check for creator option + creator := upath.Query().Get("creator") + if creator != "" { + address, err := crypto.AddressFromBech32(creator) + if err != nil { + var ok bool + address, ok = bk.GetByName(creator) + if !ok { + return qpath, fmt.Errorf("invalid name or address for creator %q", creator) + } + } + + qpath.Creator = address + } + + // Check for deposit option + deposit := upath.Query().Get("deposit") + if deposit != "" { + coins, err := std.ParseCoins(deposit) + if err != nil { + return qpath, fmt.Errorf( + "unable to parse deposit amount %q (should be in the form xxxugnot): %w", deposit, err, + ) + } + + qpath.Deposit = coins + } + + return qpath, nil +} diff --git a/contribs/gnodev/pkg/dev/query_path_test.go b/contribs/gnodev/pkg/dev/query_path_test.go new file mode 100644 index 00000000000..519edcc7739 --- /dev/null +++ b/contribs/gnodev/pkg/dev/query_path_test.go @@ -0,0 +1,132 @@ +package dev_test + +import ( + "testing" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolvePackageModifierQuery(t *testing.T) { + validAddr := crypto.MustAddressFromString(integration.DefaultAccount_Address) + validBech32Addr := validAddr.String() + validCoins := std.MustParseCoins("100ugnot") + + tests := []struct { + name string + path string + book *address.Book + wantQuery dev.QueryPath + wantErrMsg string + }{ + { + name: "valid creator bech32", + path: "abc.xy/some/path?creator=" + validBech32Addr, + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/some/path", + Creator: validAddr, + }, + }, + + { + name: "valid creator name", + path: "abc.xy/path?creator=alice", + book: func() *address.Book { + bk := address.NewBook() + bk.Add(validAddr, "alice") + return bk + }(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/path", + Creator: validAddr, + }, + }, + + { + name: "invalid creator", + path: "abc.xy/path?creator=bob", + book: address.NewBook(), + wantErrMsg: `invalid name or address for creator "bob"`, + }, + + { + name: "invalid bech32 creator", + path: "abc.xy/path?creator=invalid", + book: address.NewBook(), + wantErrMsg: `invalid name or address for creator "invalid"`, + }, + + { + name: "valid deposit", + path: "abc.xy/path?deposit=100ugnot", + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/path", + Deposit: validCoins, + }, + }, + + { + name: "invalid deposit", + path: "abc.xy/path?deposit=invalid", + book: address.NewBook(), + wantErrMsg: `unable to parse deposit amount "invalid" (should be in the form xxxugnot)`, + }, + + { + name: "both creator and deposit", + path: "abc.xy/path?creator=" + validBech32Addr + "&deposit=100ugnot", + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/path", + Creator: validAddr, + Deposit: validCoins, + }, + }, + + { + name: "malformed path", + path: "://invalid", + book: address.NewBook(), + wantErrMsg: "malformed path/query", + }, + + { + name: "no creator or deposit", + path: "abc.xy/path", + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/path", + }, + }, + + { + name: "clean path with ..", + path: "abc.xy/foo/../bar", + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/bar", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotQuery, err := dev.ResolveQueryPath(tt.book, tt.path) + if tt.wantErrMsg != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErrMsg) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantQuery, gotQuery) + }) + } +} diff --git a/contribs/gnodev/pkg/emitter/server.go b/contribs/gnodev/pkg/emitter/server.go index 3e32984268d..29f8e3050ba 100644 --- a/contribs/gnodev/pkg/emitter/server.go +++ b/contribs/gnodev/pkg/emitter/server.go @@ -1,6 +1,7 @@ package emitter import ( + "encoding/json" "log/slog" "net/http" "sync" @@ -32,6 +33,10 @@ func NewServer(logger *slog.Logger) *Server { } } +func (s *Server) LockEmit() { s.muClients.Lock() } + +func (s *Server) UnlockEmit() { s.muClients.Unlock() } + // ws handler func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) @@ -69,13 +74,9 @@ func (s *Server) emit(evt events.Event) { s.muClients.Lock() defer s.muClients.Unlock() - jsonEvt := EventJSON{evt.Type(), evt} - - s.logger.Info("sending event to clients", - "clients", len(s.clients), - "type", evt.Type(), - "event", evt) + s.logEvent(evt) + jsonEvt := EventJSON{evt.Type(), evt} for conn := range s.clients { err := conn.WriteJSON(jsonEvt) if err != nil { @@ -96,3 +97,15 @@ func (s *Server) conns() []*websocket.Conn { return conns } + +func (s *Server) logEvent(evt events.Event) { + var logEvt string + if rawEvt, err := json.Marshal(evt); err == nil { + logEvt = string(rawEvt) + } + + s.logger.Info("sending event to clients", + "clients", len(s.clients), + "type", evt.Type(), + "event", logEvt) +} diff --git a/contribs/gnodev/pkg/emitter/static/hotreload.js b/contribs/gnodev/pkg/emitter/static/hotreload.js index 28e47c1ea15..7b58fc35004 100644 --- a/contribs/gnodev/pkg/emitter/static/hotreload.js +++ b/contribs/gnodev/pkg/emitter/static/hotreload.js @@ -1,19 +1,34 @@ -(function() { +document.addEventListener('DOMContentLoaded', function() { // Define the events that will trigger a page reload const eventsReload = [ {{range .ReloadEvents}}'{{.}}',{{end}} ]; - + // Establish the WebSocket connection to the event server const ws = new WebSocket('ws://{{- .Remote -}}'); - + // `gracePeriod` mitigates reload loops due to excessive events. This period // occurs post-loading and lasts for the `graceTimeout` duration. const graceTimeout = 1000; // ms let gracePeriod = true; let debounceTimeout = setTimeout(function() { gracePeriod = false; - }, graceTimeout); + }, graceTimeout); + + // Flag to track if a link click is in progress + let clickInProgress = false; + + // Capture clicks on tags to prevent reloading appening when clicking on link + document.addEventListener('click', function(event) { + const target = event.target; + if (target.tagName === 'A' && target.href) { + clickInProgress = true; + // Wait a bit before allowing reload again + setTimeout(function() { + clickInProgress = false; + }, 5000); + } + }); // Handle incoming WebSocket messages ws.onmessage = function(event) { @@ -23,19 +38,21 @@ // Ignore events not in the reload-triggering list if (!eventsReload.includes(message.type)) { - return; + return; } - // Reload the page immediately if we're not in the grace period - if (!gracePeriod) { + // Reload the page immediately if we're not in the grace period and no clicks are in progress + if (!gracePeriod && !clickInProgress) { window.location.reload(); return; } - // If still in the grace period, debounce the reload + // If still in the grace period or a click is in progress, debounce the reload clearTimeout(debounceTimeout); debounceTimeout = setTimeout(function() { - window.location.reload(); + if (!clickInProgress) { + window.location.reload(); + } }, graceTimeout); } catch (e) { @@ -50,4 +67,4 @@ ws.onclose = function() { console.log('WebSocket connection closed'); }; -})(); +}); diff --git a/contribs/gnodev/pkg/logger/log_column.go b/contribs/gnodev/pkg/logger/log_column.go index 2a720525903..0e6c181ad6d 100644 --- a/contribs/gnodev/pkg/logger/log_column.go +++ b/contribs/gnodev/pkg/logger/log_column.go @@ -21,7 +21,9 @@ func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *Co }) // Default column output - defaultOutput := newColumeWriter(lipgloss.NewStyle(), "", w) + renderer := lipgloss.NewRenderer(nil, termenv.WithProfile(profile)) + + defaultOutput := newColumeWriter(w, lipgloss.NewStyle(), "") charmLogger.SetOutput(defaultOutput) charmLogger.SetStyles(defaultStyles()) charmLogger.SetColorProfile(profile) @@ -40,10 +42,12 @@ func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *Co } return &ColumnLogger{ - Logger: charmLogger, - writer: w, - prefix: charmLogger.GetPrefix(), - colors: map[string]lipgloss.Color{}, + Logger: charmLogger, + writer: w, + prefix: charmLogger.GetPrefix(), + colors: map[string]lipgloss.Color{}, + colorProfile: profile, + renderer: renderer, } } @@ -52,6 +56,7 @@ type ColumnLogger struct { prefix string writer io.Writer + renderer *lipgloss.Renderer colorProfile termenv.Profile colors map[string]lipgloss.Color @@ -72,10 +77,11 @@ func (cl *ColumnLogger) WithGroup(group string) slog.Handler { // generate bright color based on the group name fg = colorFromString(group, 0.5, 0.6) } - baseStyle := lipgloss.NewStyle().Foreground(fg) + + baseStyle := lipgloss.NewStyle().Foreground(fg).Renderer(cl.renderer) nlog := cl.Logger.With() // clone logger - nlog.SetOutput(newColumeWriter(baseStyle, group, cl.writer)) + nlog.SetOutput(newColumeWriter(cl.writer, baseStyle, group)) nlog.SetColorProfile(cl.colorProfile) return &ColumnLogger{ Logger: nlog, @@ -99,7 +105,7 @@ type columnWriter struct { writer io.Writer } -func newColumeWriter(baseStyle lipgloss.Style, prefix string, writer io.Writer) *columnWriter { +func newColumeWriter(w io.Writer, baseStyle lipgloss.Style, prefix string) *columnWriter { const width = 12 style := baseStyle. @@ -112,7 +118,7 @@ func newColumeWriter(baseStyle lipgloss.Style, prefix string, writer io.Writer) prefix = prefix[:width-3] + "..." } - return &columnWriter{style: style, prefix: prefix, writer: writer} + return &columnWriter{style: style, prefix: prefix, writer: w} } func (cl *columnWriter) Write(buf []byte) (n int, err error) { diff --git a/contribs/gnodev/pkg/packages/glob.go b/contribs/gnodev/pkg/packages/glob.go new file mode 100644 index 00000000000..1b76425deb4 --- /dev/null +++ b/contribs/gnodev/pkg/packages/glob.go @@ -0,0 +1,214 @@ +// Inspired by: https://cs.opensource.google/go/x/tools/+/master:gopls/internal/test/integration/fake/glob/glob.go + +package packages + +import ( + "errors" + "fmt" + "strings" +) + +var ErrAdjacentSlash = errors.New("** may only be adjacent to '/'") + +// Glob patterns can have the following syntax: +// - `*` to match one or more characters in a path segment +// - `**` to match any number of path segments, including none +// +// Expanding on this: +// - '/' matches one or more literal slashes. +// - any other character matches itself literally. +type Glob struct { + elems []element // pattern elements +} + +// Parse builds a Glob for the given pattern, returning an error if the pattern +// is invalid. +func Parse(pattern string) (*Glob, error) { + g, _, err := parse(pattern) + return g, err +} + +func parse(pattern string) (*Glob, string, error) { + g := new(Glob) + for len(pattern) > 0 { + switch pattern[0] { + case '/': + // Skip consecutive slashes + for len(pattern) > 0 && pattern[0] == '/' { + pattern = pattern[1:] + } + g.elems = append(g.elems, slash{}) + + case '*': + if len(pattern) > 1 && pattern[1] == '*' { + if (len(g.elems) > 0 && g.elems[len(g.elems)-1] != slash{}) || (len(pattern) > 2 && pattern[2] != '/') { + return nil, "", ErrAdjacentSlash + } + pattern = pattern[2:] + g.elems = append(g.elems, starStar{}) + break + } + pattern = pattern[1:] + g.elems = append(g.elems, star{}) + + default: + pattern = g.parseLiteral(pattern) + } + } + return g, "", nil +} + +func (g *Glob) parseLiteral(pattern string) string { + end := strings.IndexAny(pattern, "*/") + if end == -1 { + end = len(pattern) + } + g.elems = append(g.elems, literal(pattern[:end])) + return pattern[end:] +} + +func (g *Glob) String() string { + var b strings.Builder + for _, e := range g.elems { + fmt.Fprint(&b, e) + } + return b.String() +} + +func (g *Glob) StarFreeBase() string { + var b strings.Builder + for _, e := range g.elems { + if e == (star{}) || e == (starStar{}) { + break + } + fmt.Fprint(&b, e) + } + return b.String() +} + +// element holds a glob pattern element, as defined below. +type element fmt.Stringer + +// element types. +type ( + slash struct{} // One or more '/' separators + literal string // string literal, not containing / or * + star struct{} // * + starStar struct{} // ** +) + +func (s slash) String() string { return "/" } +func (l literal) String() string { return string(l) } +func (s star) String() string { return "*" } +func (s starStar) String() string { return "**" } + +// Match reports whether the input string matches the glob pattern. +func (g *Glob) Match(input string) bool { + return match(g.elems, input) +} + +func match(elems []element, input string) (ok bool) { + var elem interface{} + for len(elems) > 0 { + elem, elems = elems[0], elems[1:] + switch elem := elem.(type) { + case slash: + // Skip consecutive slashes in the input + if len(input) == 0 || input[0] != '/' { + return false + } + for len(input) > 0 && input[0] == '/' { + input = input[1:] + } + + case starStar: + // Special cases: + // - **/a matches "a" + // - **/ matches everything + // + // Note that if ** is followed by anything, it must be '/' (this is + // enforced by Parse). + if len(elems) > 0 { + elems = elems[1:] + } + + // A trailing ** matches anything. + if len(elems) == 0 { + return true + } + + // Backtracking: advance pattern segments until the remaining pattern + // elements match. + for len(input) != 0 { + if match(elems, input) { + return true + } + _, input = split(input) + } + return false + + case literal: + if !strings.HasPrefix(input, string(elem)) { + return false + } + input = input[len(elem):] + + case star: + var segInput string + segInput, input = split(input) + + elemEnd := len(elems) + for i, e := range elems { + if e == (slash{}) { + elemEnd = i + break + } + } + segElems := elems[:elemEnd] + elems = elems[elemEnd:] + + // A trailing * matches the entire segment. + if len(segElems) == 0 { + if len(elems) > 0 && elems[0] == (slash{}) { + elems = elems[1:] // shift elems + } + break + } + + // Backtracking: advance characters until remaining subpattern elements + // match. + matched := false + for i := range segInput { + if match(segElems, segInput[i:]) { + matched = true + break + } + } + if !matched { + return false + } + + default: + panic(fmt.Sprintf("segment type %T not implemented", elem)) + } + } + + return len(input) == 0 +} + +// split returns the portion before and after the first slash +// (or sequence of consecutive slashes). If there is no slash +// it returns (input, nil). +func split(input string) (first, rest string) { + i := strings.IndexByte(input, '/') + if i < 0 { + return input, "" + } + first = input[:i] + for j := i; j < len(input); j++ { + if input[j] != '/' { + return first, input[j:] + } + } + return first, "" +} diff --git a/contribs/gnodev/pkg/packages/glob_test.go b/contribs/gnodev/pkg/packages/glob_test.go new file mode 100644 index 00000000000..7fad4eb2fe1 --- /dev/null +++ b/contribs/gnodev/pkg/packages/glob_test.go @@ -0,0 +1,93 @@ +// Inspired by: https://cs.opensource.google/go/x/tools/+/master:gopls/internal/test/integration/fake/glob/glob_test.go + +package packages + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMatch(t *testing.T) { + t.Parallel() + + tests := []struct { + pattern, input string + want bool + }{ + // Basic cases. + {"", "", true}, + {"", "a", false}, + {"", "/", false}, + {"abc", "abc", true}, + + // ** behavior + {"**", "abc", true}, + {"**/abc", "abc", true}, + {"**", "abc/def", true}, + + // * behavior + {"/*", "/a", true}, + {"*", "foo", true}, + {"*o", "foo", true}, + {"*o", "foox", false}, + {"f*o", "foo", true}, + {"f*o", "fo", true}, + + // Dirs cases + {"**/", "path/to/foo/", true}, + {"**/", "path/to/foo", true}, + + {"path/to/foo", "path/to/foo", true}, + {"path/to/foo", "path/to/bar", false}, + {"path/*/foo", "path/to/foo", true}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/522/foo", true}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/722/foo", false}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/522/bar", false}, + {"path/*/foo", "path/to/to/foo", false}, + {"path/**/foo", "path/to/to/foo", true}, + {"path/**/foo", "path/to/to/bar", false}, + {"path/**/foo", "path/foo", true}, + {"**/abc/**", "foo/r/x/abc/bar", true}, + + // Realistic examples. + {"**/*.ts", "path/to/foo.ts", true}, + {"**/*.js", "path/to/foo.js", true}, + {"**/*.go", "path/to/foo.go", true}, + } + + for _, test := range tests { + g, err := Parse(test.pattern) + require.NoErrorf(t, err, "Parse(%q) failed unexpectedly: %v", test.pattern, err) + assert.Equalf(t, test.want, g.Match(test.input), + "Parse(%q).Match(%q) = %t, want %t", test.pattern, test.input, !test.want, test.want) + } +} + +func TestBaseFreeStar(t *testing.T) { + t.Parallel() + + tests := []struct { + pattern, baseFree string + }{ + // Basic cases. + {"", ""}, + {"foo", "foo"}, + {"foo/bar", "foo/bar"}, + {"foo///bar", "foo/bar"}, + {"foo/bar/", "foo/bar/"}, + {"foo/bar/*/*/z", "foo/bar/"}, + {"foo/bar/**", "foo/bar/"}, + {"**", ""}, + {"/**", "/"}, + } + + for _, test := range tests { + g, err := Parse(test.pattern) + require.NoErrorf(t, err, "Parse(%q) failed unexpectedly: %v", test.pattern, err) + got := g.StarFreeBase() + assert.Equalf(t, test.baseFree, got, + "Parse(%q).Match(%q) = %q, want %q", test.pattern, test.baseFree, got, test.baseFree) + } +} diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go new file mode 100644 index 00000000000..3bc978721e6 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader.go @@ -0,0 +1,12 @@ +package packages + +type Loader interface { + // Load resolves package package paths and all their dependencies in the correct order. + Load(paths ...string) ([]Package, error) + + // Resolve processes a single package path and returns the corresponding Package. + Resolve(path string) (*Package, error) + + // Name of the loader + Name() string +} diff --git a/contribs/gnodev/pkg/packages/loader_base.go b/contribs/gnodev/pkg/packages/loader_base.go new file mode 100644 index 00000000000..039932bd400 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader_base.go @@ -0,0 +1,104 @@ +package packages + +import ( + "errors" + "fmt" + "go/parser" + "go/token" +) + +type BaseLoader struct { + Resolver +} + +func NewLoader(res ...Resolver) *BaseLoader { + return &BaseLoader{ChainResolvers(res...)} +} + +func (l BaseLoader) Name() string { + return l.Resolver.Name() +} + +func (l BaseLoader) Load(paths ...string) ([]Package, error) { + fset := token.NewFileSet() + visited, stack := map[string]bool{}, map[string]bool{} + pkgs := make([]Package, 0) + for _, root := range paths { + deps, err := load(root, fset, l.Resolver, visited, stack) + if err != nil { + return nil, err + } + pkgs = append(pkgs, deps...) + } + + return pkgs, nil +} + +func (l BaseLoader) Resolve(path string) (*Package, error) { + fset := token.NewFileSet() + return l.Resolver.Resolve(fset, path) +} + +func load(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { + if stack[path] { + return nil, fmt.Errorf("cycle detected: %s", path) + } + if visited[path] { + return nil, nil + } + + visited[path] = true + + mempkg, err := resolver.Resolve(fset, path) + if err != nil { + if errors.Is(err, ErrResolverPackageSkip) { + return nil, nil + } + + return nil, fmt.Errorf("unable to resolve package %q: %w", path, err) + } + + var name string + imports := map[string]struct{}{} + for _, file := range mempkg.Files { + fname := file.Name + if !isGnoFile(fname) || isTestFile(fname) { + continue + } + + f, err := parser.ParseFile(fset, fname, file.Body, parser.ImportsOnly) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", file.Name, err) + } + + if name != "" && name != f.Name.Name { + return nil, fmt.Errorf("conflict package name between %q and %q", name, f.Name.Name) + } + + for _, imp := range f.Imports { + if len(imp.Path.Value) <= 2 { + continue + } + + val := imp.Path.Value[1 : len(imp.Path.Value)-1] + imports[val] = struct{}{} + } + + name = f.Name.Name + } + + pkgs := []Package{} + for imp := range imports { + subDeps, err := load(imp, fset, resolver, visited, stack) + if err != nil { + return nil, fmt.Errorf("importing %q: %w", imp, err) + } + + pkgs = append(pkgs, subDeps...) + } + pkgs = append(pkgs, *mempkg) + + stack[path] = false + + return pkgs, nil +} diff --git a/contribs/gnodev/pkg/packages/loader_glob.go b/contribs/gnodev/pkg/packages/loader_glob.go new file mode 100644 index 00000000000..dabfe613574 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader_glob.go @@ -0,0 +1,94 @@ +package packages + +import ( + "fmt" + "go/token" + "io/fs" + "os" + "path/filepath" + "strings" +) + +type GlobLoader struct { + Root string + Resolver Resolver +} + +func NewGlobLoader(rootpath string, res ...Resolver) *GlobLoader { + return &GlobLoader{rootpath, ChainResolvers(res...)} +} + +func (l GlobLoader) Name() string { + return l.Resolver.Name() +} + +func (l GlobLoader) MatchPaths(globs ...string) ([]string, error) { + if l.Root == "" { + return globs, nil + } + + if _, err := os.Stat(l.Root); err != nil { + return nil, fmt.Errorf("unable to stat root: %w", err) + } + + mpaths := []string{} + for _, input := range globs { + cleanInput := filepath.Clean(input) + gpath, err := Parse(cleanInput) + if err != nil { + return nil, fmt.Errorf("invalid glob path %q: %w", input, err) + } + + base := gpath.StarFreeBase() + if base == cleanInput { + mpaths = append(mpaths, base) + continue + } + + // root := filepath.Join(l.Root, base) + root := l.Root + err = filepath.WalkDir(root, func(dirpath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + relPath, relErr := filepath.Rel(root, dirpath) + if relErr != nil { + return relErr + } + + if !d.IsDir() { + return nil + } + + if strings.HasPrefix(d.Name(), ".") { + return fs.SkipDir + } + + if gpath.Match(relPath) { + mpaths = append(mpaths, relPath) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("walking directory %q: %w", root, err) + } + } + + return mpaths, nil +} + +func (l GlobLoader) Load(gpaths ...string) ([]Package, error) { + paths, err := l.MatchPaths(gpaths...) + if err != nil { + return nil, fmt.Errorf("match glob pattern error: %w", err) + } + + loader := &BaseLoader{Resolver: l.Resolver} + return loader.Load(paths...) +} + +func (l GlobLoader) Resolve(path string) (*Package, error) { + return l.Resolver.Resolve(token.NewFileSet(), path) +} diff --git a/contribs/gnodev/pkg/packages/loader_test.go b/contribs/gnodev/pkg/packages/loader_test.go new file mode 100644 index 00000000000..1fa338587b0 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader_test.go @@ -0,0 +1,83 @@ +package packages + +import ( + "testing" + + "github.com/gnolang/gno/gnovm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoader_LoadWithDeps(t *testing.T) { + t.Parallel() + + fsresolver := NewRootResolver("./testdata") + loader := NewLoader(fsresolver) + + // package c depend on package b + pkgs, err := loader.Load(TestdataPkgC) + require.NoError(t, err) + require.Len(t, pkgs, 3) + for i, path := range []string{TestdataPkgA, TestdataPkgB, TestdataPkgC} { + assert.Equal(t, path, pkgs[i].Path) + } +} + +func TestLoader_ResolverPriority(t *testing.T) { + t.Parallel() + + const commonPath = "abc.yz/pkg/a" + + pkgA := gnovm.MemPackage{Name: "pkga", Path: commonPath} + resolverA := NewMockResolver(&pkgA) + + pkgB := gnovm.MemPackage{Name: "pkgb", Path: commonPath} + resolverB := NewMockResolver(&pkgB) + + t.Run("pkgA then pkgB", func(t *testing.T) { + t.Parallel() + + loader := NewLoader(resolverA, resolverB) + pkg, err := loader.Resolve(commonPath) + require.NoError(t, err) + require.Equal(t, pkgA.Name, pkg.Name) + require.Equal(t, commonPath, pkg.Path) + }) + + t.Run("pkgB then pkgA", func(t *testing.T) { + t.Parallel() + + loader := NewLoader(resolverB, resolverA) + pkg, err := loader.Resolve(commonPath) + require.NoError(t, err) + require.Equal(t, pkgB.Name, pkg.Name) + require.Equal(t, commonPath, pkg.Path) + }) +} + +func TestLoader_Glob(t *testing.T) { + const root = "./testdata" + cases := []struct { + GlobPath string + PkgResults []string + }{ + {"abc.xy/pkg/*", []string{TestdataPkgA, TestdataPkgB, TestdataPkgC}}, + {"abc.xy/nested/*", []string{TestdataNestedA}}, + {"abc.xy/**/cc", []string{TestdataNestedC, TestdataPkgA, TestdataPkgB, TestdataPkgC}}, + {"abc.xy/*/aa", []string{TestdataNestedA, TestdataPkgA}}, + } + + fsresolver := NewRootResolver("./testdata") + globloader := NewGlobLoader("./testdata", fsresolver) + + for _, tc := range cases { + t.Run(tc.GlobPath, func(t *testing.T) { + pkgs, err := globloader.Load(tc.GlobPath) + require.NoError(t, err) + require.Len(t, pkgs, len(tc.PkgResults)) + for i, expected := range tc.PkgResults { + assert.Equal(t, expected, pkgs[i].Path) + } + }) + } +} diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go new file mode 100644 index 00000000000..d6aa532ce64 --- /dev/null +++ b/contribs/gnodev/pkg/packages/package.go @@ -0,0 +1,102 @@ +package packages + +import ( + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" +) + +type PackageKind int + +const ( + PackageKindOther = iota + PackageKindRemote = iota + PackageKindFS +) + +type Package struct { + gnovm.MemPackage + Kind PackageKind + Location string +} + +func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) { + modpath := filepath.Join(dir, "gno.mod") + if _, err := os.Stat(modpath); err == nil { + draft, err := isDraftFile(modpath) + if err != nil { + return nil, err + } + + // Skip draft package + // XXX: We could potentially do that in a middleware, but doing this + // here avoid to potentially parse broken files + if draft { + return nil, ErrResolverPackageSkip + } + } + + mempkg, err := gnolang.ReadMemPackage(dir, path) + switch { + case err == nil: // ok + case os.IsNotExist(err): + return nil, ErrResolverPackageNotFound + default: + return nil, fmt.Errorf("unable to read package %q: %w", dir, err) + } + + if err := validateMemPackage(fset, mempkg); err != nil { + return nil, err + } + + return &Package{ + MemPackage: *mempkg, + Location: dir, + Kind: PackageKindFS, + }, nil +} + +func validateMemPackage(fset *token.FileSet, mempkg *gnovm.MemPackage) error { + if mempkg.IsEmpty() { + return fmt.Errorf("empty package: %w", ErrResolverPackageSkip) + } + + // Validate package name + for _, file := range mempkg.Files { + if !isGnoFile(file.Name) || isTestFile(file.Name) { + continue + } + + f, err := parser.ParseFile(fset, file.Name, file.Body, parser.PackageClauseOnly) + if err != nil { + return fmt.Errorf("unable to parse file %q: %w", file.Name, err) + } + + if f.Name.Name != mempkg.Name { + return fmt.Errorf("%q package name conflict, expected %q found %q", + mempkg.Path, mempkg.Name, f.Name.Name) + } + } + + return nil +} + +func isDraftFile(modpath string) (bool, error) { + modfile, err := os.ReadFile(modpath) + if err != nil { + return false, fmt.Errorf("unable to read file %q: %w", modpath, err) + } + + mod, err := gnomod.Parse(modpath, modfile) + if err != nil { + return false, fmt.Errorf("unable to parse `gno.mod`: %w", err) + } + + return mod.Draft, nil +} diff --git a/contribs/gnodev/pkg/packages/resolver.go b/contribs/gnodev/pkg/packages/resolver.go new file mode 100644 index 00000000000..9ed9269b6d8 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver.go @@ -0,0 +1,234 @@ +package packages + +import ( + "errors" + "fmt" + "go/parser" + "go/scanner" + "go/token" + "log/slog" + "strings" + "time" +) + +var ( + ErrResolverPackageNotFound = errors.New("package not found") + ErrResolverPackageSkip = errors.New("package has been skip") +) + +type Resolver interface { + Name() string + Resolve(fset *token.FileSet, path string) (*Package, error) +} + +type NoopResolver struct{} + +func (NoopResolver) Name() string { return "" } +func (NoopResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + return nil, ErrResolverPackageNotFound +} + +// Chain Resolver + +type ChainedResolver []Resolver + +func ChainResolvers(rs ...Resolver) Resolver { + switch len(rs) { + case 0: + return &NoopResolver{} + case 1: + return rs[0] + default: + return ChainedResolver(rs) + } +} + +func (cr ChainedResolver) Name() string { + names := make([]string, 0, len(cr)) + for _, r := range cr { + rname := r.Name() + if rname == "" { + continue + } + + names = append(names, rname) + } + + return strings.Join(names, "/") +} + +func (cr ChainedResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + for _, resolver := range cr { + pkg, err := resolver.Resolve(fset, path) + if err == nil { + return pkg, nil + } else if errors.Is(err, ErrResolverPackageNotFound) { + continue + } + + return nil, fmt.Errorf("resolver %q error: %w", resolver.Name(), err) + } + + return nil, ErrResolverPackageNotFound +} + +type MiddlewareHandler func(fset *token.FileSet, path string, next Resolver) (*Package, error) + +type middlewareResolver struct { + Handler MiddlewareHandler + Next Resolver +} + +func MiddlewareResolver(r Resolver, handlers ...MiddlewareHandler) Resolver { + // Start with the final resolver + start := r + + // Wrap each handler around the previous one + for _, handler := range handlers { + start = &middlewareResolver{ + Next: start, + Handler: handler, + } + } + + return start +} + +func (r middlewareResolver) Name() string { + return r.Next.Name() +} + +func (r *middlewareResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + if r.Handler != nil { + return r.Handler(fset, path, r.Next) + } + + return r.Next.Resolve(fset, path) +} + +// LogMiddleware creates a logging middleware handler. +func LogMiddleware(logger *slog.Logger) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + start := time.Now() + pkg, err := next.Resolve(fset, path) + switch { + case err == nil: + logger.Debug("path resolved", + "resolver", next.Name(), + "path", path, + "name", pkg.Name, + "took", time.Since(start).String(), + "location", pkg.Location, + ) + case errors.Is(err, ErrResolverPackageSkip): + logger.Debug(err.Error(), + "resolver", next.Name(), + "path", path, + "took", time.Since(start).String(), + ) + + case errors.Is(err, ErrResolverPackageNotFound): + logger.Warn(err.Error(), + "resolver", next.Name(), + "path", path, + "took", time.Since(start).String()) + + default: + logger.Error(err.Error(), + "resolver", next.Name(), + "path", path, + "took", time.Since(start).String()) + } + + return pkg, err + } +} + +type ShouldCacheFunc func(pkg *Package) bool + +func CacheAll(_ *Package) bool { return true } + +// CacheMiddleware creates a caching middleware handler. +func CacheMiddleware(shouldCache ShouldCacheFunc) MiddlewareHandler { + cacheMap := make(map[string]*Package) + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + if pkg, ok := cacheMap[path]; ok { + return pkg, nil + } + + pkg, err := next.Resolve(fset, path) + if pkg != nil && shouldCache(pkg) { + cacheMap[path] = pkg + } + + return pkg, err + } +} + +// FilterPathHandler defines the function signature for filter handlers. +type FilterPathHandler func(path string) bool + +func FilterPathMiddleware(name string, filter FilterPathHandler) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + if filter(path) { + return nil, fmt.Errorf("filter %q: %w", name, ErrResolverPackageSkip) + } + + return next.Resolve(fset, path) + } +} + +var FilterStdlibs = FilterPathMiddleware("stdlibs", isStdPath) + +func isStdPath(path string) bool { + if i := strings.IndexRune(path, '/'); i > 0 { + if j := strings.IndexRune(path[:i], '.'); j >= 0 { + return false + } + } + + return true +} + +// PackageCheckerMiddleware creates a middleware handler for post-processing syntax. +func PackageCheckerMiddleware(logger *slog.Logger) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + // First, resolve the package using the next resolver in the chain. + pkg, err := next.Resolve(fset, path) + if err != nil { + return nil, err + } + + if err := pkg.Validate(); err != nil { + return nil, fmt.Errorf("invalid package %q: %w", path, err) + } + + // Post-process each file in the package. + for _, file := range pkg.Files { + fname := file.Name + if !isGnoFile(fname) { + continue + } + + logger.Debug("checking syntax", "path", path, "filename", fname) + _, err := parser.ParseFile(fset, file.Name, file.Body, parser.AllErrors) + if err == nil { + continue + } + + if el, ok := err.(scanner.ErrorList); ok { + for _, e := range el { + logger.Error("syntax error", + "path", path, + "filename", fname, + "err", e.Error(), + ) + } + } + + return nil, fmt.Errorf("file %q have error(s)", file.Name) + } + + return pkg, nil + } +} diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go new file mode 100644 index 00000000000..13448aca52d --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -0,0 +1,39 @@ +package packages + +import ( + "fmt" + "go/token" + "path/filepath" + "strings" +) + +type LocalResolver struct { + Path string + Dir string +} + +func NewLocalResolver(path, dir string) *LocalResolver { + return &LocalResolver{ + Path: path, + Dir: dir, + } +} + +func (r *LocalResolver) Name() string { + return fmt.Sprintf("local<%s>", filepath.Base(r.Dir)) +} + +func (r LocalResolver) IsValid() bool { + pkg, err := r.Resolve(token.NewFileSet(), r.Path) + return err == nil && pkg != nil +} + +func (r LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + after, found := strings.CutPrefix(path, r.Path) + if !found { + return nil, ErrResolverPackageNotFound + } + + dir := filepath.Join(r.Dir, after) + return ReadPackageFromDir(fset, path, dir) +} diff --git a/contribs/gnodev/pkg/packages/resolver_mock.go b/contribs/gnodev/pkg/packages/resolver_mock.go new file mode 100644 index 00000000000..f6a09af8883 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_mock.go @@ -0,0 +1,40 @@ +package packages + +import ( + "go/token" + + "github.com/gnolang/gno/gnovm" +) + +type MockResolver struct { + pkgs map[string]*gnovm.MemPackage + resolveCalls map[string]int // Track resolve calls per path +} + +func NewMockResolver(pkgs ...*gnovm.MemPackage) *MockResolver { + mappkgs := make(map[string]*gnovm.MemPackage, len(pkgs)) + for _, pkg := range pkgs { + mappkgs[pkg.Path] = pkg + } + return &MockResolver{ + pkgs: mappkgs, + resolveCalls: make(map[string]int), + } +} + +func (m *MockResolver) ResolveCalls(fset *token.FileSet, path string) int { + count, _ := m.resolveCalls[path] + return count +} + +func (m *MockResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + m.resolveCalls[path]++ // Increment call count + if mempkg, ok := m.pkgs[path]; ok { + return &Package{MemPackage: *mempkg}, nil + } + return nil, ErrResolverPackageNotFound +} + +func (m *MockResolver) Name() string { + return "mock" +} diff --git a/contribs/gnodev/pkg/packages/resolver_remote.go b/contribs/gnodev/pkg/packages/resolver_remote.go new file mode 100644 index 00000000000..94396f70c83 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_remote.go @@ -0,0 +1,94 @@ +package packages + +import ( + "bytes" + "errors" + "fmt" + "go/parser" + "go/token" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type remoteResolver struct { + *client.RPCClient + name string + fset *token.FileSet +} + +func NewRemoteResolver(name string, cl *client.RPCClient) Resolver { + return &remoteResolver{ + RPCClient: cl, + name: name, + fset: token.NewFileSet(), + } +} + +func (res *remoteResolver) Name() string { + return fmt.Sprintf("remote<%s>", res.name) +} + +func (res *remoteResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + const qpath = "vm/qfile" + + // First query files + data := []byte(path) + qres, err := res.RPCClient.ABCIQuery(qpath, data) + if err != nil { + return nil, fmt.Errorf("client unable to query: %w", err) + } + + if err := qres.Response.Error; err != nil { + if errors.Is(err, vm.InvalidPkgPathError{}) || + strings.HasSuffix(err.Error(), "is not available") { // XXX: find a better to check this + return nil, ErrResolverPackageNotFound + } + + return nil, fmt.Errorf("querying %q error: %w", path, err) + } + + var name string + memFiles := []*gnovm.MemFile{} + files := bytes.Split(qres.Response.Data, []byte{'\n'}) + for _, filename := range files { + fname := string(filename) + fpath := filepath.Join(path, fname) + qres, err := res.RPCClient.ABCIQuery(qpath, []byte(fpath)) + if err != nil { + return nil, fmt.Errorf("unable to query path") + } + + if err := qres.Response.Error; err != nil { + return nil, fmt.Errorf("unable to query file %q on path %q: %w", fname, path, err) + } + body := qres.Response.Data + + // Check package name + if name == "" && isGnoFile(fname) && !isTestFile(fname) { + // Check package name + f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + } + name = f.Name.Name + } + + memFiles = append(memFiles, &gnovm.MemFile{ + Name: fname, Body: string(body), + }) + } + + return &Package{ + MemPackage: gnovm.MemPackage{ + Name: name, + Path: path, + Files: memFiles, + }, + Kind: PackageKindRemote, + Location: path, + }, nil +} diff --git a/contribs/gnodev/pkg/packages/resolver_remote_test.go b/contribs/gnodev/pkg/packages/resolver_remote_test.go new file mode 100644 index 00000000000..69347c0ad4d --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_remote_test.go @@ -0,0 +1 @@ +package packages diff --git a/contribs/gnodev/pkg/packages/resolver_root.go b/contribs/gnodev/pkg/packages/resolver_root.go new file mode 100644 index 00000000000..ae6a9d416ea --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_root.go @@ -0,0 +1,30 @@ +package packages + +import ( + "fmt" + "go/token" + "os" + "path/filepath" +) + +type rootResolver struct { + root string // Root folder +} + +func NewRootResolver(path string) Resolver { + return &rootResolver{root: path} +} + +func (r *rootResolver) Name() string { + return fmt.Sprintf("root<%s>", filepath.Base(r.root)) +} + +func (r *rootResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + dir := filepath.Join(r.root, path) + _, err := os.Stat(dir) + if err != nil { + return nil, ErrResolverPackageNotFound + } + + return ReadPackageFromDir(fset, path, dir) +} diff --git a/contribs/gnodev/pkg/packages/resolver_test.go b/contribs/gnodev/pkg/packages/resolver_test.go new file mode 100644 index 00000000000..9341bb80d7b --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_test.go @@ -0,0 +1,290 @@ +package packages + +import ( + "bytes" + "errors" + "go/token" + "log/slog" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLogMiddleware(t *testing.T) { + t.Parallel() + + mockResolver := NewMockResolver(&gnovm.MemPackage{ + Path: "abc.xy/test/pkg", + Name: "pkg", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: "package pkg"}, + }, + }) + + t.Run("logs package not found", func(t *testing.T) { + t.Parallel() + + var buff bytes.Buffer + + logger := slog.New(slog.NewTextHandler(&buff, &slog.HandlerOptions{})) + middleware := LogMiddleware(logger) + + resolver := MiddlewareResolver(mockResolver, middleware) + pkg, err := resolver.Resolve(token.NewFileSet(), "abc.xy/invalid/pkg") + require.Error(t, err) + require.Nil(t, pkg) + assert.Contains(t, buff.String(), "package not found") + }) + + t.Run("logs package resolution", func(t *testing.T) { + t.Parallel() + + var buff bytes.Buffer + logger := slog.New(slog.NewTextHandler(&buff, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + middleware := LogMiddleware(logger) + + resolver := MiddlewareResolver(mockResolver, middleware) + pkg, err := resolver.Resolve(token.NewFileSet(), "abc.xy/test/pkg") + require.NoError(t, err) + require.NotNil(t, pkg) + assert.Contains(t, buff.String(), "path resolved") + }) +} + +func TestCacheMiddleware(t *testing.T) { + t.Parallel() + + pkg := &gnovm.MemPackage{Path: "abc.xy/cached/pkg", Name: "pkg"} + t.Run("caches resolved packages", func(t *testing.T) { + t.Parallel() + + mockResolver := NewMockResolver(pkg) + cacheMiddleware := CacheMiddleware(CacheAll) + cachedResolver := MiddlewareResolver(mockResolver, cacheMiddleware) + + // First call + pkg1, err := cachedResolver.Resolve(token.NewFileSet(), pkg.Path) + require.NoError(t, err) + require.Equal(t, 1, mockResolver.resolveCalls[pkg.Path]) + + // Second call + pkg2, err := cachedResolver.Resolve(token.NewFileSet(), pkg.Path) + require.NoError(t, err) + require.Same(t, pkg1, pkg2) + require.Equal(t, 1, mockResolver.resolveCalls[pkg.Path]) + }) + + t.Run("no cache when shouldCache is false", func(t *testing.T) { + t.Parallel() + + mockResolver := NewMockResolver(pkg) + cacheMiddleware := CacheMiddleware(func(*Package) bool { return false }) + cachedResolver := MiddlewareResolver(mockResolver, cacheMiddleware) + + pkg1, err := cachedResolver.Resolve(token.NewFileSet(), pkg.Path) + require.NoError(t, err) + pkg2, err := cachedResolver.Resolve(token.NewFileSet(), pkg.Path) + require.NoError(t, err) + require.NotSame(t, pkg1, pkg2) + require.Equal(t, 2, mockResolver.resolveCalls[pkg.Path]) + }) +} + +func TestFilterStdlibsMiddleware(t *testing.T) { + t.Parallel() + + middleware := FilterStdlibs + mockResolver := NewMockResolver(&gnovm.MemPackage{ + Path: "abc.xy/pkg", + Name: "pkg", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: "package pkg"}, + }, + }) + filteredResolver := MiddlewareResolver(mockResolver, middleware) + + t.Run("filters stdlib paths", func(t *testing.T) { + t.Parallel() + + _, err := filteredResolver.Resolve(token.NewFileSet(), "fmt") + require.Error(t, err) + require.True(t, errors.Is(err, ErrResolverPackageSkip)) + require.Equal(t, 0, mockResolver.resolveCalls["fmt"]) + }) + + t.Run("allows non-stdlib paths", func(t *testing.T) { + t.Parallel() + + pkg, err := filteredResolver.Resolve(token.NewFileSet(), "abc.xy/pkg") + require.NoError(t, err) + require.NotNil(t, pkg) + require.Equal(t, 1, mockResolver.resolveCalls["abc.xy/pkg"]) + }) +} + +func TestPackageCheckerMiddleware(t *testing.T) { + t.Parallel() + + logger := log.NewTestingLogger(t) + t.Run("valid package syntax", func(t *testing.T) { + t.Parallel() + + validPkg := &gnovm.MemPackage{ + Path: "abc.xy/r/valid/pkg", + Name: "valid", + Files: []*gnovm.MemFile{ + {Name: "valid.gno", Body: "package valid; func Foo() {}"}, + }, + } + mockResolver := NewMockResolver(validPkg) + middleware := PackageCheckerMiddleware(logger) + resolver := MiddlewareResolver(mockResolver, middleware) + + pkg, err := resolver.Resolve(token.NewFileSet(), validPkg.Path) + require.NoError(t, err) + require.NotNil(t, pkg) + }) + + t.Run("invalid package syntax", func(t *testing.T) { + t.Parallel() + + invalidPkg := &gnovm.MemPackage{ + Path: "abc.xy/r/invalid/pkg", + Name: "invalid", + Files: []*gnovm.MemFile{ + {Name: "invalid.gno", Body: "package invalid\nfunc Foo() {"}, + }, + } + mockResolver := NewMockResolver(invalidPkg) + middleware := PackageCheckerMiddleware(logger) + resolver := MiddlewareResolver(mockResolver, middleware) + + _, err := resolver.Resolve(token.NewFileSet(), invalidPkg.Path) + require.Error(t, err) + require.Contains(t, err.Error(), `file "invalid.gno" have error(s)`) + }) + + t.Run("ignores non-gno files", func(t *testing.T) { + t.Parallel() + + nonGnoPkg := &gnovm.MemPackage{ + Path: "abc.xy/r/non/gno/pkg", + Name: "pkg", + Files: []*gnovm.MemFile{ + {Name: "README.md", Body: "# Documentation"}, + }, + } + mockResolver := NewMockResolver(nonGnoPkg) + middleware := PackageCheckerMiddleware(logger) + resolver := MiddlewareResolver(mockResolver, middleware) + + _, err := resolver.Resolve(token.NewFileSet(), nonGnoPkg.Path) + require.NoError(t, err) + }) +} + +func TestResolverLocal_Resolve(t *testing.T) { + t.Parallel() + + const anotherPath = "abc.xy/another/path" + localResolver := NewLocalResolver(anotherPath, filepath.Join("./testdata", TestdataPkgA)) + + t.Run("valid package", func(t *testing.T) { + t.Parallel() + + pkg, err := localResolver.Resolve(token.NewFileSet(), anotherPath) + require.NoError(t, err) + require.NotNil(t, pkg) + require.Equal(t, pkg.Name, "aa") + }) + + t.Run("invalid package", func(t *testing.T) { + t.Parallel() + + pkg, err := localResolver.Resolve(token.NewFileSet(), "abc.xy/wrong/package") + require.Nil(t, pkg) + require.Error(t, err) + require.ErrorIs(t, err, ErrResolverPackageNotFound) + }) +} + +func TestResolver_ResolveRemote(t *testing.T) { + const targetPath = "gno.land/r/target/path" + + mempkg := gnovm.MemPackage{ + Name: "foo", + Path: targetPath, + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo; func Render(_ string) string { return "bar" }`, + }, + {Name: "gno.mod", Body: `module ` + targetPath}, + }, + } + + rootdir := gnoenv.RootDir() + cfg := integration.TestingMinimalNodeConfig(rootdir) + logger := log.NewTestingLogger(t) + + // Setup genesis state + privKey := secp256k1.GenPrivKey() + cfg.Genesis.AppState = integration.GenerateTestingGenesisState(privKey, mempkg) + + _, address := integration.TestingInMemoryNode(t, logger, cfg) + cl, err := client.NewHTTPClient(address) + require.NoError(t, err) + + remoteResolver := NewRemoteResolver(address, cl) + t.Run("valid package", func(t *testing.T) { + pkg, err := remoteResolver.Resolve(token.NewFileSet(), mempkg.Path) + require.NoError(t, err) + require.NotNil(t, pkg) + assert.Equal(t, mempkg, pkg.MemPackage) + }) + + t.Run("invalid package", func(t *testing.T) { + pkg, err := remoteResolver.Resolve(token.NewFileSet(), "gno.land/r/not/a/valid/package") + require.Nil(t, pkg) + require.Error(t, err) + require.ErrorIs(t, err, ErrResolverPackageNotFound) + }) +} + +func TestResolverRoot_Resolve(t *testing.T) { + t.Parallel() + + fsResolver := NewRootResolver("./testdata") + t.Run("valid packages", func(t *testing.T) { + t.Parallel() + + for _, tpkg := range []string{TestdataPkgA, TestdataPkgB, TestdataPkgC} { + t.Run(tpkg, func(t *testing.T) { + pkg, err := fsResolver.Resolve(token.NewFileSet(), tpkg) + require.NoError(t, err) + require.NotNil(t, pkg) + require.Equal(t, tpkg, pkg.Path) + require.Equal(t, filepath.Base(tpkg), pkg.Name) + }) + } + }) + + t.Run("invalid packages", func(t *testing.T) { + t.Parallel() + + pkg, err := fsResolver.Resolve(token.NewFileSet(), "abc.xy/wrong/package") + require.Nil(t, pkg) + require.Error(t, err) + require.ErrorIs(t, err, ErrResolverPackageNotFound) + }) +} diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/file.gno new file mode 100644 index 00000000000..14492ef76f3 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/file.gno @@ -0,0 +1 @@ +package aa diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/gno.mod new file mode 100644 index 00000000000..071e676d43e --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/gno.mod @@ -0,0 +1 @@ +module abc.xy/nested/aa diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/file.gno new file mode 100644 index 00000000000..592f1946da0 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/file.gno @@ -0,0 +1 @@ +package bb diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/gno.mod new file mode 100644 index 00000000000..2e0f55a7954 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/gno.mod @@ -0,0 +1 @@ +module abc.xy/nested/nested/bb \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/file.gno new file mode 100644 index 00000000000..10702f6990c --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/file.gno @@ -0,0 +1 @@ +package cc diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/gno.mod new file mode 100644 index 00000000000..0932deb1366 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/gno.mod @@ -0,0 +1 @@ +module abc.xy/nested/nested/cc \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/file.gno new file mode 100644 index 00000000000..b809785a376 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/file.gno @@ -0,0 +1,3 @@ +package aa + +type SA struct{} diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/gno.mod new file mode 100644 index 00000000000..02d58054ca6 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/gno.mod @@ -0,0 +1 @@ +module abc.xy/pkg/aa \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/file.gno new file mode 100644 index 00000000000..5cca9ec3c21 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/file.gno @@ -0,0 +1,5 @@ +package bb + +import "abc.xy/pkg/aa" + +type SB = aa.SA diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/gno.mod new file mode 100644 index 00000000000..b5d760d6f75 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/gno.mod @@ -0,0 +1 @@ +module abc.xy/pkg/bb \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/file.gno new file mode 100644 index 00000000000..21819a7b686 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/file.gno @@ -0,0 +1,5 @@ +package cc + +import "abc.xy/pkg/bb" + +type SC = bb.SB diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/gno.mod new file mode 100644 index 00000000000..bc993583fd3 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/gno.mod @@ -0,0 +1 @@ +module abc.xy/pkg/cc \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata_test.go b/contribs/gnodev/pkg/packages/testdata_test.go new file mode 100644 index 00000000000..5c9a8b45cd5 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata_test.go @@ -0,0 +1,44 @@ +// This test file serves as a reference for the testdata directory tree. + +package packages + +// The structure of the testdata directory is as follows: +// +// testdata +// ├── abc.xy +// ├── nested +// │ ├── aa +// │ │ └── gno.mod +// │ └── nested +// │ ├── bb +// │ │ └── gno.mod +// │ └── cc +// │ └── gno.mod +// └── pkg +// ├── aa +// │ ├── file1.gno +// │ └── gno.mod +// ├── bb // depends on aa +// │ ├── file1.gno +// │ └── gno.mod +// └── cc // depends on bb +// ├── file1.gno +// └── gno.mod + +const ( + TestdataPkgA = "abc.xy/pkg/aa" + TestdataPkgB = "abc.xy/pkg/bb" + TestdataPkgC = "abc.xy/pkg/cc" +) + +// List of testdata package paths +var testdataPkgs = []string{TestdataPkgA, TestdataPkgB, TestdataPkgC} + +const ( + TestdataNestedA = "abc.xy/nested/aa" // Path to nested package A + TestdataNestedB = "abc.xy/nested/nested/bb" // Path to nested package B + TestdataNestedC = "abc.xy/nested/nested/cc" // Path to nested package C +) + +// List of nested package paths +var testdataNested = []string{TestdataNestedA, TestdataNestedB, TestdataNestedC} diff --git a/contribs/gnodev/pkg/packages/utils.go b/contribs/gnodev/pkg/packages/utils.go new file mode 100644 index 00000000000..93160a3a1a5 --- /dev/null +++ b/contribs/gnodev/pkg/packages/utils.go @@ -0,0 +1,14 @@ +package packages + +import ( + "path/filepath" + "strings" +) + +func isGnoFile(name string) bool { + return filepath.Ext(name) == ".gno" && !strings.HasPrefix(name, ".") +} + +func isTestFile(name string) bool { + return strings.HasSuffix(name, "_filetest.gno") || strings.HasSuffix(name, "_test.gno") +} diff --git a/contribs/gnodev/pkg/proxy/path_interceptor.go b/contribs/gnodev/pkg/proxy/path_interceptor.go new file mode 100644 index 00000000000..84d2f92b22f --- /dev/null +++ b/contribs/gnodev/pkg/proxy/path_interceptor.go @@ -0,0 +1,330 @@ +package proxy + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "path/filepath" + "strings" + "sync" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" + rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type PathHandler func(path ...string) + +type PathInterceptor struct { + proxyAddr, targetAddr net.Addr + + logger *slog.Logger + listener net.Listener + handlers []PathHandler + muHandlers sync.RWMutex +} + +// NewPathInterceptor creates a new path proxy interceptor. +func NewPathInterceptor(logger *slog.Logger, target net.Addr) (*PathInterceptor, error) { + // Create a listener on the target address + proxyListener, err := net.Listen(target.Network(), target.String()) + if err != nil { + return nil, fmt.Errorf("failed to listen on %s://%s", target.Network(), target.String()) + } + + // Find on a new random port for the target + targetListener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("failed to listen on tcp://127.0.0.1:0") + } + proxyAddr := targetListener.Addr() + // Immediately close this listener after proxy initialization + defer targetListener.Close() + + proxy := &PathInterceptor{ + listener: proxyListener, + logger: logger, + targetAddr: target, + proxyAddr: proxyAddr, + } + + go proxy.handleConnections() + + return proxy, nil +} + +// HandlePath adds a new path handler to the interceptor. +func (proxy *PathInterceptor) HandlePath(fn PathHandler) { + proxy.muHandlers.Lock() + defer proxy.muHandlers.Unlock() + proxy.handlers = append(proxy.handlers, fn) +} + +// ProxyAddress returns the network address of the proxy. +func (proxy *PathInterceptor) ProxyAddress() string { + return fmt.Sprintf("%s://%s", proxy.proxyAddr.Network(), proxy.proxyAddr.String()) +} + +// TargetAddress returns the network address of the target. +func (proxy *PathInterceptor) TargetAddress() string { + return fmt.Sprintf("%s://%s", proxy.targetAddr.Network(), proxy.targetAddr.String()) +} + +// handleConnections manages incoming connections to the proxy. +func (proxy *PathInterceptor) handleConnections() { + defer proxy.listener.Close() + + for { + conn, err := proxy.listener.Accept() + if err != nil { + if !errors.Is(err, net.ErrClosed) { + proxy.logger.Debug("failed to accept connection", "error", err) + } + + return + } + + proxy.logger.Debug("new connection", "remote", conn.RemoteAddr()) + go proxy.handleConnection(conn) + } +} + +// handleConnection processes a single connection between client and target. +func (proxy *PathInterceptor) handleConnection(inConn net.Conn) { + logger := proxy.logger.With(slog.String("in", inConn.RemoteAddr().String())) + + // Establish a connection to the target + outConn, err := net.Dial(proxy.proxyAddr.Network(), proxy.proxyAddr.String()) + if err != nil { + logger.Error("target connection failed", "target", proxy.proxyAddr.String(), "error", err) + inConn.Close() + return + } + logger = logger.With(slog.String("out", outConn.RemoteAddr().String())) + + // Coordinate connection closure + var closeOnce sync.Once + closeConnections := func() { + inConn.Close() + outConn.Close() + } + + // Setup bidirectional copying + var wg sync.WaitGroup + wg.Add(2) + + // Response path (target -> client) + go func() { + defer wg.Done() + defer closeOnce.Do(closeConnections) + + _, err := io.Copy(inConn, outConn) + if err == nil || errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { + return // Connection has been closed + } + + logger.Debug("response copy error", "error", err) + }() + + // Request path (client -> target) + go func() { + defer wg.Done() + defer closeOnce.Do(closeConnections) + + var buffer bytes.Buffer + tee := io.TeeReader(inConn, &buffer) + reader := bufio.NewReader(tee) + + // Process HTTP requests + if err := proxy.processHTTPRequests(reader, &buffer, outConn); err != nil { + if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { + return // Connection has been closed + } + + if _, isNetError := err.(net.Error); isNetError { + logger.Debug("request processing error", "error", err) + return + } + + // Continue processing the connection if not a network error + } + + // Forward remaining data after HTTP processing + if buffer.Len() > 0 { + if _, err := outConn.Write(buffer.Bytes()); err != nil { + logger.Debug("buffer flush failed", "error", err) + } + } + + // Directly pipe remaining traffic + if _, err := io.Copy(outConn, inConn); err != nil && !errors.Is(err, net.ErrClosed) { + logger.Debug("raw copy failed", "error", err) + } + }() + + wg.Wait() + logger.Debug("connection closed") +} + +// processHTTPRequests handles the HTTP request/response cycle. +func (proxy *PathInterceptor) processHTTPRequests(reader *bufio.Reader, buffer *bytes.Buffer, outConn net.Conn) error { + for { + request, err := http.ReadRequest(reader) + if err != nil { + return fmt.Errorf("read request failed: %w", err) + } + + // Check for websocket upgrade + if isWebSocket(request) { + return errors.New("websocket upgrade requested") + } + + // Read and process the request body + body, err := io.ReadAll(request.Body) + request.Body.Close() + if err != nil { + return fmt.Errorf("body read failed: %w", err) + } + + if err := proxy.handleRequest(body); err != nil { + proxy.logger.Debug("request handler warning", "error", err) + } + + // Forward the original request bytes + if _, err := outConn.Write(buffer.Bytes()); err != nil { + return fmt.Errorf("request forward failed: %w", err) + } + + buffer.Reset() // Prepare for the next request + } +} + +func isWebSocket(req *http.Request) bool { + return strings.EqualFold(req.Header.Get("Upgrade"), "websocket") +} + +type uniqPaths map[string]struct{} + +func (upaths uniqPaths) list() []string { + paths := make([]string, 0, len(upaths)) + for p := range upaths { + paths = append(paths, p) + } + return paths +} + +func (upaths uniqPaths) add(path string) { upaths[path] = struct{}{} } + +// handleRequest parses and processes the RPC request body. +func (proxy *PathInterceptor) handleRequest(body []byte) error { + ps := make(uniqPaths) + if err := parseRPCRequest(body, ps); err != nil { + return fmt.Errorf("unable to parse RPC request: %w", err) + } + + paths := ps.list() + if len(paths) == 0 { + return nil + } + + proxy.logger.Debug("parsed request paths", "paths", paths) + + proxy.muHandlers.RLock() + defer proxy.muHandlers.RUnlock() + + for _, handle := range proxy.handlers { + handle(paths...) + } + + return nil +} + +// Close closes the proxy listener. +func (proxy *PathInterceptor) Close() error { + return proxy.listener.Close() +} + +// parseRPCRequest unmarshals and processes RPC requests, returning paths. +func parseRPCRequest(body []byte, upaths uniqPaths) error { + var req rpctypes.RPCRequest + if err := json.Unmarshal(body, &req); err != nil { + return fmt.Errorf("unable to unmarshal RPC request: %w", err) + } + + switch req.Method { + case "abci_query": + var squery struct { + Path string `json:"path"` + Data []byte `json:"data,omitempty"` + } + if err := json.Unmarshal(req.Params, &squery); err != nil { + return fmt.Errorf("unable to unmarshal params: %w", err) + } + + return handleQuery(squery.Path, squery.Data, upaths) + + case "broadcast_tx_commit": + var stx struct { + Tx []byte `json:"tx"` + } + if err := json.Unmarshal(req.Params, &stx); err != nil { + return fmt.Errorf("unable to unmarshal params: %w", err) + } + + return handleTx(stx.Tx, upaths) + } + + return fmt.Errorf("unhandled method: %q", req.Method) +} + +// handleTx processes the transaction and returns relevant paths. +func handleTx(bz []byte, upaths uniqPaths) error { + var tx std.Tx + if err := amino.Unmarshal(bz, &tx); err != nil { + return fmt.Errorf("unable to unmarshal tx: %w", err) + } + + for _, msg := range tx.Msgs { + switch msg := msg.(type) { + case vm.MsgAddPackage: // MsgAddPackage should not be handled + case vm.MsgCall: + upaths.add(msg.PkgPath) + case vm.MsgRun: + upaths.add(msg.Package.Path) + } + } + + return nil +} + +// handleQuery processes the query and returns relevant paths. +func handleQuery(path string, data []byte, upaths uniqPaths) error { + switch path { + case ".app/simulate": + return handleTx(data, upaths) + + case "vm/qrender", "vm/qfile", "vm/qfuncs", "vm/qeval": + path, _, _ := strings.Cut(string(data), ":") // Cut arguments out + path = filepath.Clean(path) + + // If path is a file, grab the directory instead + if ext := filepath.Ext(path); ext != "" { + path = filepath.Dir(path) + } + + upaths.add(path) + return nil + + default: + return fmt.Errorf("unhandled: %q", path) + } + + // XXX: handle more cases +} diff --git a/contribs/gnodev/pkg/proxy/path_interceptor_test.go b/contribs/gnodev/pkg/proxy/path_interceptor_test.go new file mode 100644 index 00000000000..c7082adfa30 --- /dev/null +++ b/contribs/gnodev/pkg/proxy/path_interceptor_test.go @@ -0,0 +1,179 @@ +package proxy_test + +import ( + "net" + "net/http" + "path/filepath" + "testing" + + "github.com/gnolang/gno/contribs/gnodev/pkg/proxy" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProxy(t *testing.T) { + const targetPath = "gno.land/r/target/foo" + + pkg := gnovm.MemPackage{ + Name: "foo", + Path: targetPath, + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo; func Render(_ string) string { return "foo" }`, + }, + {Name: "gno.mod", Body: `module ` + targetPath}, + }, + } + + rootdir := gnoenv.RootDir() + cfg := integration.TestingMinimalNodeConfig(rootdir) + logger := log.NewTestingLogger(t) + + tmp := t.TempDir() + sock := filepath.Join(tmp, "node.sock") + addr, err := net.ResolveUnixAddr("unix", sock) + require.NoError(t, err) + + // Create proxy + interceptor, err := proxy.NewPathInterceptor(logger, addr) + require.NoError(t, err) + defer interceptor.Close() + cfg.TMConfig.RPC.ListenAddress = interceptor.ProxyAddress() + cfg.SkipGenesisVerification = true + + // Setup genesis + privKey := secp256k1.GenPrivKey() + cfg.Genesis.AppState = integration.GenerateTestingGenesisState(privKey, pkg) + creator := privKey.PubKey().Address() + + integration.TestingInMemoryNode(t, logger, cfg) + pathChan := make(chan []string, 1) + interceptor.HandlePath(func(paths ...string) { + pathChan <- paths + }) + + // ---- Test Cases ---- + + t.Run("valid_vm_query", func(t *testing.T) { + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + + res, err := cli.ABCIQuery("vm/qrender", []byte(targetPath+":\n")) + require.NoError(t, err) + assert.Nil(t, res.Response.Error) + + select { + case paths := <-pathChan: + require.Len(t, paths, 1) + assert.Equal(t, []string{targetPath}, paths) + default: + t.Fatal("paths not captured") + } + }) + + t.Run("valid_vm_query_file", func(t *testing.T) { + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + + res, err := cli.ABCIQuery("vm/qfile", []byte(filepath.Join(targetPath, "foo.gno"))) + require.NoError(t, err) + assert.Nil(t, res.Response.Error) + + select { + case paths := <-pathChan: + require.Len(t, paths, 1) + assert.Equal(t, []string{targetPath}, paths) + default: + t.Fatal("paths not captured") + } + }) + + t.Run("simulate_tx_paths", func(t *testing.T) { + // Build transaction with multiple messages + var tx std.Tx + send := std.MustParseCoins(ugnot.ValueString(10_000_000)) + tx.Fee = std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}} + tx.Msgs = []std.Msg{ + vm.NewMsgCall(creator, send, targetPath, "Render", []string{""}), + vm.NewMsgCall(creator, send, targetPath, "Render", []string{""}), + vm.NewMsgCall(creator, send, targetPath, "Render", []string{""}), + } + + bytes, err := tx.GetSignBytes(cfg.Genesis.ChainID, 0, 0) + require.NoError(t, err) + signature, err := privKey.Sign(bytes) + require.NoError(t, err) + tx.Signatures = []std.Signature{{PubKey: privKey.PubKey(), Signature: signature}} + + bz, err := amino.Marshal(tx) + require.NoError(t, err) + + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + + res, err := cli.BroadcastTxCommit(bz) + require.NoError(t, err) + assert.NoError(t, res.CheckTx.Error) + assert.NoError(t, res.DeliverTx.Error) + + select { + case paths := <-pathChan: + require.Len(t, paths, 1) + assert.Equal(t, []string{targetPath}, paths) + default: + t.Fatal("paths not captured") + } + }) + + t.Run("websocket_forward", func(t *testing.T) { + // For now simply try to connect and upgrade the connection + // XXX: fully support ws + + conn, err := net.Dial(addr.Network(), addr.String()) + require.NoError(t, err) + defer conn.Close() + + // Send WebSocket handshake + req, _ := http.NewRequest("GET", "http://"+interceptor.TargetAddress(), nil) + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Connection", "Upgrade") + err = req.Write(conn) + require.NoError(t, err) + }) + + t.Run("invalid_query_data", func(t *testing.T) { + // Making a valid call but not supported by the proxy + // should succeed + query := "auth/accounts/" + creator.String() + + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + defer cli.Close() + + res, err := cli.ABCIQuery(query, []byte{}) + require.NoError(t, err) + require.NoError(t, res.Response.Error) + + var qret struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(res.Response.Data, &qret) + require.NoError(t, err) + assert.Equal(t, qret.BaseAccount.Address, creator) + + select { + case paths := <-pathChan: + require.FailNowf(t, "should not catch a path", "catched: %+v", paths) + default: + } + }) +} diff --git a/contribs/gnodev/pkg/rawterm/keypress.go b/contribs/gnodev/pkg/rawterm/keypress.go index 45c64c999dd..e9c1728bd4b 100644 --- a/contribs/gnodev/pkg/rawterm/keypress.go +++ b/contribs/gnodev/pkg/rawterm/keypress.go @@ -26,6 +26,12 @@ const ( KeyN KeyPress = 'N' KeyP KeyPress = 'P' KeyR KeyPress = 'R' + + // Special keys + KeyUp KeyPress = 0x80 // Arbitrary value outside ASCII range + KeyDown KeyPress = 0x81 + KeyLeft KeyPress = 0x82 + KeyRight KeyPress = 0x83 ) func (k KeyPress) Upper() KeyPress { @@ -52,6 +58,14 @@ func (k KeyPress) String() string { return "Ctrl+S" case KeyCtrlT: return "Ctrl+T" + case KeyUp: + return "Up Arrow" + case KeyDown: + return "Down Arrow" + case KeyLeft: + return "Left Arrow" + case KeyRight: + return "Right Arrow" default: // For printable ASCII characters if k > 0x20 && k < 0x7e { diff --git a/contribs/gnodev/pkg/rawterm/rawterm.go b/contribs/gnodev/pkg/rawterm/rawterm.go index 58b8dde1530..7ff4cadaf94 100644 --- a/contribs/gnodev/pkg/rawterm/rawterm.go +++ b/contribs/gnodev/pkg/rawterm/rawterm.go @@ -54,12 +54,31 @@ func (rt *RawTerm) read(buf []byte) (n int, err error) { } func (rt *RawTerm) ReadKeyPress() (KeyPress, error) { - buf := make([]byte, 1) - if _, err := rt.read(buf); err != nil { + buf := make([]byte, 3) + n, err := rt.read(buf) + if err != nil { return KeyNone, err } - return KeyPress(buf[0]), nil + if n == 1 && buf[0] != '\x1b' { + // Single character, not an escape sequence + return KeyPress(buf[0]), nil + } + + if n >= 3 && buf[0] == '\x1b' && buf[1] == '[' { + switch buf[2] { + case 'A': + return KeyUp, nil + case 'B': + return KeyDown, nil + case 'C': + return KeyRight, nil + case 'D': + return KeyLeft, nil + } + } + + return KeyNone, fmt.Errorf("unknown key sequence: %v", buf[:n]) } // writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. diff --git a/contribs/gnodev/pkg/watcher/watch.go b/contribs/gnodev/pkg/watcher/watch.go index 63158a06c4b..5f277fd6646 100644 --- a/contribs/gnodev/pkg/watcher/watch.go +++ b/contribs/gnodev/pkg/watcher/watch.go @@ -5,15 +5,14 @@ import ( "fmt" "log/slog" "path/filepath" - "sort" "strings" "time" emitter "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" events "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/fsnotify/fsnotify" - "github.com/gnolang/gno/gnovm/pkg/gnomod" ) type PackageWatcher struct { @@ -25,7 +24,6 @@ type PackageWatcher struct { logger *slog.Logger watcher *fsnotify.Watcher - pkgsDir []string emitter emitter.Emitter } @@ -39,7 +37,6 @@ func NewPackageWatcher(logger *slog.Logger, emitter emitter.Emitter) (*PackageWa p := &PackageWatcher{ ctx: ctx, stop: cancel, - pkgsDir: []string{}, logger: logger, watcher: watcher, emitter: emitter, @@ -114,58 +111,61 @@ func (p *PackageWatcher) Stop() { p.stop() } -// AddPackages adds new packages to the watcher. -// Packages are sorted by their length in descending order to facilitate easier -// and more efficient matching with corresponding paths. The longest paths are -// compared first. -func (p *PackageWatcher) AddPackages(pkgs ...gnomod.Pkg) error { +func (p *PackageWatcher) UpdatePackagesWatch(pkgs ...packages.Package) { + watchList := p.watcher.WatchList() + + oldPkgs := make(map[string]struct{}, len(watchList)) + for _, path := range watchList { + oldPkgs[path] = struct{}{} + } + + newPkgs := make(map[string]struct{}, len(pkgs)) for _, pkg := range pkgs { - dir := pkg.Dir + if pkg.Kind != packages.PackageKindFS { + continue + } - abs, err := filepath.Abs(dir) + path, err := filepath.Abs(pkg.Location) if err != nil { - return fmt.Errorf("unable to get absolute path of %q: %w", dir, err) + p.logger.Error("Unable to get absolute path", "path", pkg.Location, "error", err) + continue } - // Use binary search to find the correct insertion point - index := sort.Search(len(p.pkgsDir), func(i int) bool { - return len(p.pkgsDir[i]) <= len(dir) // Longest paths first - }) + newPkgs[path] = struct{}{} + } - // Check for duplicates - if index < len(p.pkgsDir) && p.pkgsDir[index] == dir { - continue // Skip + for path := range oldPkgs { + if _, exists := newPkgs[path]; !exists { + p.watcher.Remove(path) + p.logger.Debug("Watcher list: removed", "path", path) } + } - // Insert the package - p.pkgsDir = append(p.pkgsDir[:index], append([]string{abs}, p.pkgsDir[index:]...)...) - - // Add the package to the watcher and handle any errors - if err := p.watcher.Add(abs); err != nil { - return fmt.Errorf("unable to watch %q: %w", pkg.Dir, err) + for path := range newPkgs { + if _, exists := oldPkgs[path]; !exists { + p.watcher.Add(path) + p.logger.Debug("Watcher list: added", "path", path) } } - - return nil } func (p *PackageWatcher) generatePackagesUpdateList(paths []string) PackageUpdateList { pkgsUpdate := []events.PackageUpdate{} mpkgs := map[string]*events.PackageUpdate{} // Pkg -> Update + watchList := p.watcher.WatchList() for _, path := range paths { - for _, pkg := range p.pkgsDir { - dirPath := filepath.Dir(path) + for _, pkg := range watchList { + if len(pkg) == len(path) { + continue // Skip if pkg == path + } // Check if a package directory contain our path directory + dirPath := filepath.Dir(path) if !strings.HasPrefix(pkg, dirPath) { continue } - if len(pkg) == len(path) { - continue // Skip if pkg == path - } - // Accumulate file updates for each package pkgu, ok := mpkgs[pkg] if !ok { diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index b5ee98614f3..cfad9919506 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -75,6 +75,7 @@ func (h *WebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + w.Header().Add("Content-Type", "text/html; charset=utf-8") h.Get(w, r) } diff --git a/gno.land/pkg/integration/node_testing.go b/gno.land/pkg/integration/node_testing.go index c613048ebd7..1af699f014d 100644 --- a/gno.land/pkg/integration/node_testing.go +++ b/gno.land/pkg/integration/node_testing.go @@ -8,6 +8,8 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -186,3 +188,30 @@ func DefaultTestingTMConfig(gnoroot string) *tmcfg.Config { tmconfig.P2P.ListenAddress = defaultListner return tmconfig } + +func GenerateTestingGenesisState(creator crypto.PrivKey, pkgs ...gnovm.MemPackage) gnoland.GnoGenesisState { + txs := make([]gnoland.TxWithMetadata, len(pkgs)) + for i, pkg := range pkgs { + // Create transaction + var tx std.Tx + tx.Fee = std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}} + tx.Msgs = []std.Msg{ + vmm.MsgAddPackage{ + Creator: creator.PubKey().Address(), + Package: &pkg, + }, + } + + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + txs[i] = gnoland.TxWithMetadata{Tx: tx} + } + + gnoland.SignGenesisTxs(txs, creator, "tendermint_test") + return gnoland.GnoGenesisState{ + Txs: txs, + Balances: []gnoland.Balance{{ + Address: creator.PubKey().Address(), + Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), + }}, + } +} diff --git a/gno.land/pkg/integration/node_testing_test.go b/gno.land/pkg/integration/node_testing_test.go new file mode 100644 index 00000000000..96b40bc0ec7 --- /dev/null +++ b/gno.land/pkg/integration/node_testing_test.go @@ -0,0 +1,75 @@ +package integration + +import ( + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateTestingGenesisState(t *testing.T) { + // Generate a test private key and address + privKey := secp256k1.GenPrivKey() + creatorAddr := privKey.PubKey().Address() + + // Create sample packages + pkg1 := gnovm.MemPackage{ + Name: "pkg1", + Path: "pkg1", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: "package1"}, + }, + } + pkg2 := gnovm.MemPackage{ + Name: "pkg2", + Path: "pkg2", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: "package2"}, + }, + } + + t.Run("single package genesis", func(t *testing.T) { + genesis := GenerateTestingGenesisState(privKey, pkg1) + + // Verify transactions + require.Len(t, genesis.Txs, 1) + tx := genesis.Txs[0].Tx + + // Check the transaction's message + require.Len(t, tx.Msgs, 1) + msg, ok := tx.Msgs[0].(vm.MsgAddPackage) + require.True(t, ok, "expected MsgAddPackage") + assert.Equal(t, pkg1, *msg.Package, "package mismatch") + + // Verify transaction signatures + require.Len(t, tx.Signatures, 1) + assert.NotEmpty(t, tx.Signatures[0], "signature should not be empty") + + // Verify balances + require.Len(t, genesis.Balances, 1) + balance := genesis.Balances[0] + assert.Equal(t, creatorAddr, balance.Address) + assert.Equal(t, std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), balance.Amount) + }) + + t.Run("multiple packages genesis", func(t *testing.T) { + genesis := GenerateTestingGenesisState(privKey, pkg1, pkg2) + + // Verify two transactions are created + require.Len(t, genesis.Txs, 2) + + // Check each transaction's package + for i, expectedPkg := range []gnovm.MemPackage{pkg1, pkg2} { + tx := genesis.Txs[i].Tx + require.Len(t, tx.Msgs, 1) + msg, ok := tx.Msgs[0].(vm.MsgAddPackage) + require.True(t, ok, "expected MsgAddPackage") + assert.Equal(t, expectedPkg, *msg.Package, "package mismatch in tx %d", i) + } + }) +} diff --git a/gno.land/pkg/keyscli/run.go b/gno.land/pkg/keyscli/run.go index 00b2be585c6..2d7f754203e 100644 --- a/gno.land/pkg/keyscli/run.go +++ b/gno.land/pkg/keyscli/run.go @@ -106,11 +106,12 @@ func execMakeRun(cfg *MakeRunCfg, args []string, cmdio commands.IO) error { } } } + + memPkg.Name = "main" if memPkg.IsEmpty() { panic(fmt.Sprintf("found an empty package %q", memPkg.Path)) } - memPkg.Name = "main" // Set to empty; this will be automatically set by the VM keeper. memPkg.Path = "" diff --git a/gnovm/memfile.go b/gnovm/memfile.go index 6988c893dd7..a08e89579ad 100644 --- a/gnovm/memfile.go +++ b/gnovm/memfile.go @@ -34,7 +34,7 @@ func (mempkg *MemPackage) GetFile(name string) *MemFile { } func (mempkg *MemPackage) IsEmpty() bool { - return len(mempkg.Files) == 0 + return mempkg.Name == "" || len(mempkg.Files) == 0 } const pathLengthLimit = 256 diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 445968a2c9c..0e8f701dea8 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -1332,12 +1332,13 @@ func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, e }) } + memPkg.Name = string(pkgName) + // If no .gno files are present, package simply does not exist. if !memPkg.IsEmpty() { if err := validatePkgName(string(pkgName)); err != nil { return nil, err } - memPkg.Name = string(pkgName) } return memPkg, nil diff --git a/tm2/pkg/commands/command.go b/tm2/pkg/commands/command.go index aa717b62ad9..a7f80b69a70 100644 --- a/tm2/pkg/commands/command.go +++ b/tm2/pkg/commands/command.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "io" "os" "strings" "text/tabwriter" @@ -31,26 +32,28 @@ func HelpExec(_ context.Context, _ []string) error { // Metadata contains basic help // information about a command type Metadata struct { - Name string - ShortUsage string - ShortHelp string - LongHelp string - Options []ff.Option + Name string + ShortUsage string + ShortHelp string + LongHelp string + Options []ff.Option + NoParentFlags bool } // Command is a simple wrapper for gnoland commands. type Command struct { - name string - shortUsage string - shortHelp string - longHelp string - options []ff.Option - cfg Config - flagSet *flag.FlagSet - subcommands []*Command - exec ExecMethod - selected *Command - args []string + name string + shortUsage string + shortHelp string + longHelp string + options []ff.Option + cfg Config + flagSet *flag.FlagSet + subcommands []*Command + exec ExecMethod + selected *Command + args []string + noParentFlags bool } func NewCommand( @@ -59,14 +62,15 @@ func NewCommand( exec ExecMethod, ) *Command { command := &Command{ - name: meta.Name, - shortUsage: meta.ShortUsage, - shortHelp: meta.ShortHelp, - longHelp: meta.LongHelp, - options: meta.Options, - flagSet: flag.NewFlagSet(meta.Name, flag.ContinueOnError), - exec: exec, - cfg: config, + name: meta.Name, + shortUsage: meta.ShortUsage, + shortHelp: meta.ShortHelp, + longHelp: meta.LongHelp, + options: meta.Options, + noParentFlags: meta.NoParentFlags, + flagSet: flag.NewFlagSet(meta.Name, flag.ContinueOnError), + exec: exec, + cfg: config, } if config != nil { @@ -77,11 +81,17 @@ func NewCommand( return command } +// SetOutput sets the destination for usage and error messages. +// If output is nil, [os.Stderr] is used. +func (c *Command) SetOutput(output io.Writer) { + c.flagSet.SetOutput(output) +} + // AddSubCommands adds a variable number of subcommands // and registers common flags using the flagset func (c *Command) AddSubCommands(cmds ...*Command) { for _, cmd := range cmds { - if c.cfg != nil { + if c.cfg != nil && !cmd.noParentFlags { // Register the parent flagset with the child. // The syntax is not intuitive, but the flagset being // modified is the subcommand's, using the flags defined From 00ff397c4e334f7f0d92e53805601b33a4861892 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 10 Feb 2025 16:31:25 +0100 Subject: [PATCH 52/60] refactor(gnolang): move Exception to frame.go (#3596) minor refactor, just to put it in a place which I think is more appropriate rather than being at the very top of the machine.go file. --- gnovm/pkg/gnolang/frame.go | 27 +++++++++++++++++++++++++++ gnovm/pkg/gnolang/machine.go | 24 ------------------------ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/gnovm/pkg/gnolang/frame.go b/gnovm/pkg/gnolang/frame.go index 60f19979b7a..bfd0d42229e 100644 --- a/gnovm/pkg/gnolang/frame.go +++ b/gnovm/pkg/gnolang/frame.go @@ -216,3 +216,30 @@ func toConstExpTrace(cte *ConstExpr) string { return tv.T.String() } + +//---------------------------------------- +// Exception + +// Exception represents a panic that originates from a gno program. +type Exception struct { + // Value is the value passed to panic. + Value TypedValue + // Frame is used to reference the frame a panic occurred in so that recover() knows if the + // currently executing deferred function is able to recover from the panic. + Frame *Frame + + Stacktrace Stacktrace +} + +func (e Exception) Sprint(m *Machine) string { + return e.Value.Sprint(m) +} + +// UnhandledPanicError represents an error thrown when a panic is not handled in the realm. +type UnhandledPanicError struct { + Descriptor string // Description of the unhandled panic. +} + +func (e UnhandledPanicError) Error() string { + return e.Descriptor +} diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 8a640f21072..627854c9e9a 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -16,30 +16,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/store" ) -// Exception represents a panic that originates from a gno program. -type Exception struct { - // Value is the value passed to panic. - Value TypedValue - // Frame is used to reference the frame a panic occurred in so that recover() knows if the - // currently executing deferred function is able to recover from the panic. - Frame *Frame - - Stacktrace Stacktrace -} - -func (e Exception) Sprint(m *Machine) string { - return e.Value.Sprint(m) -} - -// UnhandledPanicError represents an error thrown when a panic is not handled in the realm. -type UnhandledPanicError struct { - Descriptor string // Description of the unhandled panic. -} - -func (e UnhandledPanicError) Error() string { - return e.Descriptor -} - //---------------------------------------- // Machine From 3c604b40ba6ca21d38fe6eac03767424e42879fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:42:57 +0100 Subject: [PATCH 53/60] chore(deps): bump the actions group across 1 directory with 2 updates (#3686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions group with 2 updates in the / directory: [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) and [anchore/sbom-action](https://github.com/anchore/sbom-action). Updates `sigstore/cosign-installer` from 3.7.0 to 3.8.0
Release notes

Sourced from sigstore/cosign-installer's releases.

v3.8.0

What's Changed

Full Changelog: https://github.com/sigstore/cosign-installer/compare/v3...v3.8.0

Commits

Updates `anchore/sbom-action` from 0.17.9 to 0.18.0
Release notes

Sourced from anchore/sbom-action's releases.

v0.18.0

Changes in v0.18.0

Commits
  • f325610 chore(deps): bump peter-evans/create-pull-request from 7.0.5 to 7.0.6 (#511)
  • 83a99f5 chore(deps): bump release-drafter/release-drafter from 6.0.0 to 6.1.0 (#512)
  • 9af714f chore(deps): update Syft to v1.19.0 (#513)
  • See full diff in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/releaser-master.yml | 4 ++-- .github/workflows/releaser-nightly.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index ddf51ac6683..5b6f76a9135 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -29,8 +29,8 @@ jobs: go-version-file: go.mod cache: true - - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.9 + - uses: sigstore/cosign-installer@v3.8.0 + - uses: anchore/sbom-action/download-syft@v0.18.0 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml index 47b6cabb223..62066db24a1 100644 --- a/.github/workflows/releaser-nightly.yml +++ b/.github/workflows/releaser-nightly.yml @@ -23,8 +23,8 @@ jobs: go-version-file: go.mod cache: true - - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.9 + - uses: sigstore/cosign-installer@v3.8.0 + - uses: anchore/sbom-action/download-syft@v0.18.0 - uses: docker/login-action@v3 with: From 7ad19d7776453b0b95d0e131fed31a19033a25e3 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 10 Feb 2025 16:43:42 +0100 Subject: [PATCH 54/60] fix(github-bot): reviewer requirement fixes (#3706) - deduplicate reviews, so that we only consider 1 review per author, and the one that is the most relevant (ie. the latest approval/rejection if available) - when requesting a review for the team members requirement, ensure that we don't already have reviews by people in that team before requesting it (should stop the spam of review requests on tech-staff) - for the review by user requirement, check first if we have a matching review by the given user, before requesting a review by them. --------- Co-authored-by: Antoine Eddi <5222525+aeddi@users.noreply.github.com> --- .../internal/requirements/reviewer.go | 126 +++++++---- .../internal/requirements/reviewer_test.go | 212 ++++++++++++++++-- 2 files changed, 274 insertions(+), 64 deletions(-) diff --git a/contribs/github-bot/internal/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go index 3e91c394fb7..36216b0b75c 100644 --- a/contribs/github-bot/internal/requirements/reviewer.go +++ b/contribs/github-bot/internal/requirements/reviewer.go @@ -11,6 +11,38 @@ import ( "github.com/xlab/treeprint" ) +// deduplicateReviews returns a list of reviews with at most 1 review per +// author, where approval/changes requested reviews are preferred over comments +// and later reviews are preferred over earlier ones. +func deduplicateReviews(reviews []*github.PullRequestReview) []*github.PullRequestReview { + added := make(map[string]int) + result := make([]*github.PullRequestReview, 0, len(reviews)) + for _, rev := range reviews { + idx, ok := added[rev.User.GetLogin()] + switch utils.ReviewState(rev.GetState()) { + case utils.ReviewStateApproved, utils.ReviewStateChangesRequested: + // this review changes the "approval state", and is more relevant, + // so substitute it with the previous one if it exists. + if ok { + result[idx] = rev + } else { + result = append(result, rev) + added[rev.User.GetLogin()] = len(result) - 1 + } + case utils.ReviewStateCommented: + // this review does not change the "approval state", so only append + // it if a previous review doesn't exist. + if !ok { + result = append(result, rev) + added[rev.User.GetLogin()] = len(result) - 1 + } + default: + panic(fmt.Sprintf("invalid review state %q", rev.GetState())) + } + } + return result +} + // ReviewByUserRequirement asserts that there is a review by the given user, // and if given that the review matches the desiredState. type ReviewByUserRequirement struct { @@ -28,6 +60,23 @@ func (r *ReviewByUserRequirement) IsSatisfied(pr *github.PullRequest, details tr detail += fmt.Sprintf(" (with state %q)", r.desiredState) } + // Check if user already approved this PR. + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err) + return utils.AddStatusNode(false, detail, details) + } + reviews = deduplicateReviews(reviews) + + for _, review := range reviews { + if review.GetUser().GetLogin() == r.user { + r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) + result := r.desiredState == "" || review.GetState() == r.desiredState + return utils.AddStatusNode(result, detail, details) + } + } + r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber()) + // If not a dry run, make the user a reviewer if he's not already. if !r.gh.DryRun { requested := false @@ -62,22 +111,6 @@ func (r *ReviewByUserRequirement) IsSatisfied(pr *github.PullRequest, details tr } } - // Check if user already approved this PR. - reviews, err := r.gh.ListPRReviews(pr.GetNumber()) - if err != nil { - r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err) - return utils.AddStatusNode(false, detail, details) - } - - for _, review := range reviews { - if review.GetUser().GetLogin() == r.user { - r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) - result := r.desiredState == "" || review.GetState() == r.desiredState - return utils.AddStatusNode(result, detail, details) - } - } - r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber()) - return utils.AddStatusNode(false, detail, details) } @@ -123,6 +156,14 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det return utils.AddStatusNode(false, detail, details) } + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to fetch existing reviews of pr %d: %v", pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + + reviews = deduplicateReviews(reviews) + // If not a dry run, request a team review if no member has reviewed yet, // and the team review has not been requested. if !r.gh.DryRun { @@ -144,12 +185,18 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det if !teamRequested { for _, user := range reviewers.Users { - if slices.ContainsFunc(teamMembers, func(memb *github.User) bool { - return memb.GetID() == user.GetID() - }) { + if containsUserWithLogin(teamMembers, user.GetLogin()) { usersRequested = append(usersRequested, user.GetLogin()) } } + + for _, rev := range reviews { + // if not already requested and user is a team member... + if !slices.Contains(usersRequested, rev.User.GetLogin()) && + containsUserWithLogin(teamMembers, rev.User.GetLogin()) { + usersRequested = append(usersRequested, rev.User.GetLogin()) + } + } } switch { @@ -176,33 +223,29 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det // Check how many members of this team already reviewed this PR. reviewCount := uint(0) - reviews, err := r.gh.ListPRReviews(pr.GetNumber()) - if err != nil { - r.gh.Logger.Errorf("unable to check if a member of team %s already reviewed this PR: %v", r.team, err) - return utils.AddStatusNode(false, detail, details) - } - stateStr := "" - if r.desiredState != "" { - stateStr = fmt.Sprintf("%q ", r.desiredState) - } for _, review := range reviews { - for _, member := range teamMembers { - if review.GetUser().GetLogin() == member.GetLogin() { - if desired := r.desiredState; desired == "" || desired == review.GetState() { - reviewCount += 1 - } - r.gh.Logger.Debugf( - "Member %s from team %s already reviewed PR %d with state %s (%d/%d required %sreview(s))", - member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), reviewCount, r.count, stateStr, - ) + login := review.GetUser().GetLogin() + if containsUserWithLogin(teamMembers, login) { + if desired := r.desiredState; desired == "" || desired == review.GetState() { + reviewCount += 1 } + r.gh.Logger.Debugf( + "Member %s from team %s already reviewed PR %d with state %s (%d/%d required review(s) with state %q)", + login, r.team, pr.GetNumber(), review.GetState(), reviewCount, r.count, r.desiredState, + ) } } return utils.AddStatusNode(reviewCount >= r.count, detail, details) } +func containsUserWithLogin(users []*github.User, login string) bool { + return slices.ContainsFunc(users, func(u *github.User) bool { + return u.GetLogin() == login + }) +} + // WithCount specifies the number of required reviews. // By default, this is 1. func (r *ReviewByTeamMembersRequirement) WithCount(n uint) *ReviewByTeamMembersRequirement { @@ -262,20 +305,17 @@ func (r *ReviewByOrgMembersRequirement) IsSatisfied(pr *github.PullRequest, deta r.gh.Logger.Errorf("unable to check number of reviews on this PR: %v", err) return utils.AddStatusNode(false, detail, details) } + reviews = deduplicateReviews(reviews) - stateStr := "" - if r.desiredState != "" { - stateStr = fmt.Sprintf("%q ", r.desiredState) - } for _, review := range reviews { if review.GetAuthorAssociation() == "MEMBER" { if r.desiredState == "" || review.GetState() == r.desiredState { reviewed++ } r.gh.Logger.Debugf( - "Member %s already reviewed PR %d with state %s (%d/%d required %sreviews)", + "Member %s already reviewed PR %d with state %s (%d/%d required reviews with state %q)", review.GetUser().GetLogin(), pr.GetNumber(), review.GetState(), - reviewed, r.count, stateStr, + reviewed, r.count, r.desiredState, ) } } diff --git a/contribs/github-bot/internal/requirements/reviewer_test.go b/contribs/github-bot/internal/requirements/reviewer_test.go index 235dca14034..b952294a338 100644 --- a/contribs/github-bot/internal/requirements/reviewer_test.go +++ b/contribs/github-bot/internal/requirements/reviewer_test.go @@ -16,6 +16,68 @@ import ( "github.com/xlab/treeprint" ) +func Test_deduplicateReviews(t *testing.T) { + tests := []struct { + name string + reviews []*github.PullRequestReview + expected []*github.PullRequestReview + }{ + { + name: "three different authors", + reviews: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("user2")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("user3")}, State: github.String("COMMENTED")}, + }, + expected: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("user2")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("user3")}, State: github.String("COMMENTED")}, + }, + }, + { + name: "single author - approval then comment", + reviews: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("user1")}, State: github.String("COMMENTED")}, + }, + expected: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + }, + }, + { + name: "single author - approval then changes requested", + reviews: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("user1")}, State: github.String("CHANGES_REQUESTED")}, + }, + expected: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("CHANGES_REQUESTED")}, + }, + }, + { + name: "two authors - mixed reviews", + reviews: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("userA")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("userB")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("userA")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("userB")}, State: github.String("COMMENTED")}, + }, + expected: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("userA")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("userB")}, State: github.String("CHANGES_REQUESTED")}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deduplicateReviews(tt.reviews) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestReviewByUser(t *testing.T) { t.Parallel() @@ -34,6 +96,10 @@ func TestReviewByUser(t *testing.T) { }, { User: &github.User{Login: github.String("user")}, State: github.String("APPROVED"), + }, { + // Should be ignored in favour of the following one + User: &github.User{Login: github.String("anotherOne")}, + State: github.String("APPROVED"), }, { User: &github.User{Login: github.String("anotherOne")}, State: github.String("CHANGES_REQUESTED"), @@ -138,6 +204,10 @@ func TestReviewByTeamMembers(t *testing.T) { reviews := []*github.PullRequestReview{ { + // only later review should be counted. + User: &github.User{Login: github.String("user1")}, + State: github.String("CHANGES_REQUESTED"), + }, { User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED"), }, { @@ -149,6 +219,10 @@ func TestReviewByTeamMembers(t *testing.T) { }, { User: &github.User{Login: github.String("user4")}, State: github.String("CHANGES_REQUESTED"), + }, { + // only later review should be counted. + User: &github.User{Login: github.String("user5")}, + State: github.String("APPROVED"), }, { User: &github.User{Login: github.String("user5")}, State: github.String("CHANGES_REQUESTED"), @@ -163,22 +237,113 @@ func TestReviewByTeamMembers(t *testing.T) { for _, testCase := range []struct { name string - team string - count uint - state utils.ReviewState + req *ReviewByTeamMembersRequirement + reviews []*github.PullRequestReview reviewers github.Reviewers expectedResult byte }{ - {"3/3 team members approved", "team1", 3, utils.ReviewStateApproved, reviewers, satisfied}, - {"3/3 team members approved (with user reviewers)", "team1", 3, utils.ReviewStateApproved, userReviewers, satisfied}, - {"1/1 team member approved", "team2", 1, utils.ReviewStateApproved, reviewers, satisfied}, - {"1/2 team member approved", "team2", 2, utils.ReviewStateApproved, reviewers, notSatisfied}, - {"0/1 team member approved", "team3", 1, utils.ReviewStateApproved, reviewers, notSatisfied}, - {"0/1 team member approved with request", "team3", 1, utils.ReviewStateApproved, noReviewers, notSatisfied | withRequest}, - {"team doesn't exist with request", "team4", 1, utils.ReviewStateApproved, noReviewers, notSatisfied | withRequest}, - {"3/3 team members reviewed", "team2", 3, "", reviewers, satisfied}, - {"2/2 team members rejected", "team2", 2, utils.ReviewStateChangesRequested, reviewers, satisfied}, - {"1/3 team members approved", "team2", 3, utils.ReviewStateApproved, reviewers, notSatisfied}, + { + name: "3/3 team members approved", + req: ReviewByTeamMembers(nil, "team1"). + WithCount(3). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: satisfied, + }, + { + name: "3/3 team members approved (with user reviewers)", + req: ReviewByTeamMembers(nil, "team1"). + WithCount(3). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: userReviewers, + expectedResult: satisfied, + }, + { + name: "1/1 team member approved", + req: ReviewByTeamMembers(nil, "team2"). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: satisfied, + }, + { + name: "1/2 team member approved", + req: ReviewByTeamMembers(nil, "team2"). + WithCount(2). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: notSatisfied, + }, + { + name: "0/1 team member approved", + req: ReviewByTeamMembers(nil, "team3"). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: notSatisfied, + }, + { + name: "0/1 team member reviewed with request", + req: ReviewByTeamMembers(nil, "team3"), + // Show there are no current reviews, so we actually perform the request. + reviewers: noReviewers, + expectedResult: notSatisfied | withRequest, + }, + { + name: "3/3 team member approved from review list", + req: ReviewByTeamMembers(nil, "team1"). + WithDesiredState(utils.ReviewStateApproved). + WithCount(3), + reviews: reviews, + reviewers: noReviewers, + expectedResult: satisfied, + }, + { + name: "1/2 team member approved from review list", + req: ReviewByTeamMembers(nil, "team3"). + WithDesiredState(utils.ReviewStateApproved). + WithCount(2), + reviews: reviews, + reviewers: noReviewers, + expectedResult: notSatisfied, + }, + { + name: "team doesn't exist with request", + req: ReviewByTeamMembers(nil, "team4"). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: noReviewers, + expectedResult: notSatisfied | withRequest, + }, + { + name: "3/3 team members reviewed", + req: ReviewByTeamMembers(nil, "team2"). + WithCount(3), + reviews: reviews, + reviewers: reviewers, + expectedResult: satisfied, + }, + { + name: "2/2 team members rejected", + req: ReviewByTeamMembers(nil, "team2"). + WithCount(2). + WithDesiredState(utils.ReviewStateChangesRequested), + reviews: reviews, + reviewers: reviewers, + expectedResult: satisfied, + }, + { + name: "1/3 team members approved", + req: ReviewByTeamMembers(nil, "team2"). + WithCount(3). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: notSatisfied, + }, } { t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -202,17 +367,17 @@ func TestReviewByTeamMembers(t *testing.T) { ), mock.WithRequestMatchPages( mock.EndpointPattern{ - Pattern: fmt.Sprintf("/orgs/teams/%s/members", testCase.team), + Pattern: fmt.Sprintf("/orgs/teams/%s/members", testCase.req.team), Method: "GET", }, - members[testCase.team], + members[testCase.req.team], ), mock.WithRequestMatchPages( mock.EndpointPattern{ Pattern: "/repos/pulls/0/reviews", Method: "GET", }, - reviews, + testCase.reviews, ), ) @@ -224,13 +389,13 @@ func TestReviewByTeamMembers(t *testing.T) { pr := &github.PullRequest{} details := treeprint.New() - requirement := ReviewByTeamMembers(gh, testCase.team). - WithCount(testCase.count). - WithDesiredState(testCase.state) + req := new(ReviewByTeamMembersRequirement) + *req = *testCase.req + req.gh = gh - expSatisfied := testCase.expectedResult&satisfied != 0 + expSatisfied := testCase.expectedResult&satisfied > 0 expRequested := testCase.expectedResult&withRequest > 0 - assert.Equal(t, expSatisfied, requirement.IsSatisfied(pr, details), + assert.Equal(t, expSatisfied, req.IsSatisfied(pr, details), "requirement should have a satisfied status: %t", expSatisfied) assert.True(t, utils.TestLastNodeStatus(t, expSatisfied, details), "requirement details should have a status: %t", expSatisfied) @@ -248,6 +413,11 @@ func TestReviewByOrgMembers(t *testing.T) { User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED"), AuthorAssociation: github.String("MEMBER"), + }, { + // should be ignored in favour of the following one. + User: &github.User{Login: github.String("user2")}, + State: github.String("CHANGES_REQUESTED"), + AuthorAssociation: github.String("COLLABORATOR"), }, { User: &github.User{Login: github.String("user2")}, State: github.String("APPROVED"), From 1226a2157248db9b973b01ada07c6c14ad6e4a5e Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:40:15 +0100 Subject: [PATCH 55/60] fix(gnodev): do not replay failed transaction (#3708) This PR follows #3699 to skip failed transactions on `gnodev` while reloading. --------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Morgan Bazalgette --- contribs/gnodev/pkg/dev/node.go | 43 +++++++++++-- contribs/gnodev/pkg/dev/node_test.go | 96 ++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 14 deletions(-) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 5fa9dc4e4d4..0b25234f350 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -6,6 +6,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "strings" "sync" "time" @@ -20,6 +21,7 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" @@ -241,26 +243,53 @@ func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, int64BlockNum := int64(blockNum) b, err := n.client.Block(&int64BlockNum) if err != nil { - return []gnoland.TxWithMetadata{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here + return nil, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) } + txs := b.Block.Data.Txs + + bres, err := n.client.BlockResults(&int64BlockNum) + if err != nil { + return nil, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) + } + deliverTxs := bres.Results.DeliverTxs + + // Sanity check + if len(txs) != len(deliverTxs) { + panic(fmt.Errorf("invalid block txs len (%d) vs block result txs len (%d)", + len(txs), len(deliverTxs), + )) + } + + txResults := make([]*abci.ResponseDeliverTx, len(deliverTxs)) + for i, tx := range deliverTxs { + txResults[i] = &tx + } + + // XXX: Consider replacing a failed transaction with an empty transaction + // to preserve the transaction height ? + // Note that this would also require committing instead of using the + // genesis block. + + metaTxs := make([]gnoland.TxWithMetadata, 0, len(txs)) + for i, encodedTx := range txs { + if deliverTx := deliverTxs[i]; !deliverTx.IsOK() { + continue // skip failed tx + } - txs := make([]gnoland.TxWithMetadata, len(b.Block.Data.Txs)) - for i, encodedTx := range b.Block.Data.Txs { - // fallback on std tx var tx std.Tx if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil { return nil, fmt.Errorf("unable to unmarshal tx: %w", unmarshalErr) } - txs[i] = gnoland.TxWithMetadata{ + metaTxs = append(metaTxs, gnoland.TxWithMetadata{ Tx: tx, Metadata: &gnoland.GnoTxMetadata{ Timestamp: b.BlockMeta.Header.Time.Unix(), }, - } + }) } - return txs, nil + return slices.Clip(metaTxs), nil } // GetBlockTransactions returns the transactions contained diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 89fd419a9ae..7da976bc13f 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -20,6 +20,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/keys" tm2events "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" + tm2std "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -203,7 +204,6 @@ func Render(_ string) string { return str } }, } - // Call NewDevNode with no package should work node, emitter := newTestingDevNode(t, &fooPkg) assert.Len(t, node.ListPkgs(), 1) @@ -243,6 +243,82 @@ func Render(_ string) string { return str } assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) } +func TestTxGasFailure(t *testing.T) { + fooPkg := gnovm.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo +import "strconv" + +var i int +func Inc() { i++ } // method to increment i +func Render(_ string) string { return strconv.Itoa(i) } +`, + }, + }, + } + + node, emitter := newTestingDevNode(t, &fooPkg) + assert.Len(t, node.ListPkgs(), 1) + + // Test rendering + render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + require.Equal(t, "0", render) + + // Call `Inc` to update counter + msg := vm.MsgCall{ + PkgPath: "gno.land/r/dev/foo", + Func: "Inc", + Args: nil, + Send: nil, + } + + res, err := testingCallRealm(t, node, msg) + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + + // Check for correct render update + render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + require.Equal(t, "1", render) + + // Not Enough gas wanted + callCfg := gnoclient.BaseTxCfg{ + GasFee: ugnot.ValueString(10000), // Gas fee + + // Ensure sufficient gas is provided for the transaction to be committed. + // However, avoid providing too much gas to allow the + // transaction to succeed (OutOfGasError). + GasWanted: 100_000, + } + + res, err = testingCallRealmWithConfig(t, node, callCfg, msg) + require.Error(t, err) + require.ErrorAs(t, err, &tm2std.OutOfGasError{}) + + // Transaction should be committed regardless the error + require.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult, + "(probably) not enough gas for the transaction to be committed") + + // Reload the node + err = node.Reload(context.Background()) + require.NoError(t, err) + assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + + // Check for correct render update + render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + + // Assert that the previous transaction hasn't succeeded during genesis reload + require.Equal(t, "1", render) +} + func TestTxTimestampRecover(t *testing.T) { const fooFile = ` package foo @@ -455,17 +531,23 @@ func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { t.Helper() + defaultCfg := gnoclient.BaseTxCfg{ + GasFee: ugnot.ValueString(1000000), // Gas fee + GasWanted: 3_000_000, // Gas wanted + } + + return testingCallRealmWithConfig(t, node, defaultCfg, msgs...) +} + +func testingCallRealmWithConfig(t *testing.T, node *Node, bcfg gnoclient.BaseTxCfg, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { + t.Helper() + signer := newInMemorySigner(t, node.Config().ChainID()) cli := gnoclient.Client{ Signer: signer, RPCClient: node.Client(), } - txcfg := gnoclient.BaseTxCfg{ - GasFee: ugnot.ValueString(1000000), // Gas fee - GasWanted: 3_000_000, // Gas wanted - } - // Set Caller in the msgs caller, err := signer.Info() require.NoError(t, err) @@ -474,7 +556,7 @@ func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args)) } - return cli.Call(txcfg, vmMsgs...) + return cli.Call(bcfg, vmMsgs...) } func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig { From febedbf1605d887a972cc9b91b93243f24bcbb5e Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Mon, 10 Feb 2025 18:55:41 +0100 Subject: [PATCH 56/60] feat(gnovm/pkg/gnolang): add compile time guards to fail on 32-bit architectures (#3643) This change adds build constraints so as to panic if built for 32-bit architectures as it is a project wide decision not to support them. This allows the mainnet launch without sweating trying to make a bunch of runtime changes for the gnovm. Updates #3288 --------- Co-authored-by: Morgan Bazalgette --- .github/goreleaser.yaml | 288 +---------------------- gnovm/pkg/gnolang/README.md | 3 +- gnovm/pkg/gnolang/nocompile_on_32bits.go | 10 + 3 files changed, 13 insertions(+), 288 deletions(-) create mode 100644 gnovm/pkg/gnolang/nocompile_on_32bits.go diff --git a/.github/goreleaser.yaml b/.github/goreleaser.yaml index 71a8ba98745..95f7fdaab4c 100644 --- a/.github/goreleaser.yaml +++ b/.github/goreleaser.yaml @@ -23,10 +23,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnoland main: ./gno.land/cmd/gnoland binary: gnoland @@ -38,10 +34,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnokey main: ./gno.land/cmd/gnokey binary: gnokey @@ -53,10 +45,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnoweb main: ./gno.land/cmd/gnoweb binary: gnoweb @@ -68,10 +56,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnofaucet dir: ./contribs/gnofaucet binary: gnofaucet @@ -83,10 +67,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" # Gno Contribs # NOTE: Contribs binary will be added in a single docker image below: gnocontribs - id: gnobro @@ -100,10 +80,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnogenesis dir: ./contribs/gnogenesis binary: gnogenesis @@ -115,10 +91,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" gomod: proxy: true @@ -188,48 +160,6 @@ dockers: - examples - gnovm/stdlibs - gnovm/tests/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-armv6" - build_flag_templates: - - "--target=gno" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-armv7" - build_flag_templates: - - "--target=gno" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs # gnoland - use: buildx @@ -274,51 +204,7 @@ dockers: - gno.land/genesis/genesis_txs.jsonl - examples - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-armv6" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-armv7" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - + # gnokey - use: buildx dockerfile: Dockerfile.release @@ -352,40 +238,6 @@ dockers: - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-armv6" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-armv7" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey # gnoweb - use: buildx @@ -420,40 +272,6 @@ dockers: - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-armv6" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-armv7" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb # gnofaucet - use: buildx @@ -488,40 +306,6 @@ dockers: - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnofaucet - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv6" - build_flag_templates: - - "--target=gnofaucet" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnofaucet" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnofaucet - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv7" - build_flag_templates: - - "--target=gnofaucet" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnofaucet" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnofaucet # gnocontribs - use: buildx @@ -568,52 +352,6 @@ dockers: - gno.land/genesis/genesis_txs.jsonl - examples - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv6" - build_flag_templates: - - "--target=gnocontribs" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnobro - - gnogenesis - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv7" - build_flag_templates: - - "--target=gnocontribs" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnobro - - gnogenesis - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs docker_manifests: # https://goreleaser.com/customization/docker_manifest/ @@ -623,84 +361,60 @@ docker_manifests: image_templates: - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7 - name_template: ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-armv7 # gnoland - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7 - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-armv7 # gnokey - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7 - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-armv7 # gnoweb - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7 - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-armv7 # gnofaucet - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-armv7 - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv7 # gnocontribs - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv7 - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv7 docker_signs: - cmd: cosign diff --git a/gnovm/pkg/gnolang/README.md b/gnovm/pkg/gnolang/README.md index 56ac6e0baba..2cd7c2b27d2 100644 --- a/gnovm/pkg/gnolang/README.md +++ b/gnovm/pkg/gnolang/README.md @@ -1,3 +1,4 @@ # Gnolang -TODO: dedicated README +## Declarations +* Gno is only available for 64-bit architectures! diff --git a/gnovm/pkg/gnolang/nocompile_on_32bits.go b/gnovm/pkg/gnolang/nocompile_on_32bits.go new file mode 100644 index 00000000000..03cb0855876 --- /dev/null +++ b/gnovm/pkg/gnolang/nocompile_on_32bits.go @@ -0,0 +1,10 @@ +package gnolang + +import "strconv" + +func _() { + // Restricting Gno to compile only on 64-bit architectures. + // Please see https://github.com/gnolang/gno/issues/3288 + var x [1]struct{} + _ = x[strconv.IntSize-64] +} From 31fcf6aa3bdd9afc0c49f2dad33e1222b4989893 Mon Sep 17 00:00:00 2001 From: Mason McBride <56214403+masonmcbride@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:41:30 -0800 Subject: [PATCH 57/60] refactor(rpc): handlers.go - makeJSONRPCHandler() control flow refactor (#3716) I am proposing a simpler way to write the makeJSONRPCHandler function. There was a recent bug due to this function that turned lists of requests with a single item to a object that is a response--but not a list of responses. With this refactor, this function would be more resilient to bugs because of its simplified control flow: it first tries to unmarshal as a slice of requests, and returns an array of responses if successful. It then tries to unmarshal it into a single request, and returns a single response if successful. This refactor also abstracts the logic that processes the requests out to `processRequests(r, request, funcMap, logger)` to see if they actually need to be sent (eg. notifications do not need to be sent). All of that logic is now in the processRequest function, and improves on the previous implementation because now makeJSONRPCHandle is much easier to read and understand. If both unmarshals fail, the function returns an error "error unmarshalling request," same as before. Please let me know what you think of the refactor. Thanks. --- tm2/pkg/bft/rpc/lib/server/handlers.go | 122 +++++++++++++------------ 1 file changed, 66 insertions(+), 56 deletions(-) diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index b91db806342..8269bc8bf8a 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -140,74 +140,84 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger *slog.Logger) http.H return } - // first try to unmarshal the incoming request as an array of RPC requests - var ( - requests types.RPCRequests - responses types.RPCResponses - ) - - // isRPCRequestArray is used to determine if the incoming payload is an array of requests. - // This flag helps decide whether to return an array of responses (for batch requests) or a single response. - isRPCRequestArray := true + // --- Branch 1: Attempt to Unmarshal as a Batch (Slice) of Requests --- + var requests types.RPCRequests + if err := json.Unmarshal(b, &requests); err == nil { + var responses types.RPCResponses + for _, req := range requests { + if resp := processRequest(r, req, funcMap, logger); resp != nil { + responses = append(responses, *resp) + } + } - if err := json.Unmarshal(b, &requests); err != nil { - // next, try to unmarshal as a single request - var request types.RPCRequest - if err := json.Unmarshal(b, &request); err != nil { - WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request"))) + if len(responses) > 0 { + WriteRPCResponseArrayHTTP(w, responses) return } - requests = []types.RPCRequest{request} - isRPCRequestArray = false } - for _, request := range requests { - request := request - // A Notification is a Request object without an "id" member. - // The Server MUST NOT reply to a Notification, including those that are within a batch request. - if request.ID == types.JSONRPCStringID("") { - logger.Debug("HTTPJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") - continue - } - if len(r.URL.Path) > 1 { - responses = append(responses, types.RPCInvalidRequestError(request.ID, errors.New("path %s is invalid", r.URL.Path))) - continue - } - rpcFunc, ok := funcMap[request.Method] - if !ok || rpcFunc.ws { - responses = append(responses, types.RPCMethodNotFoundError(request.ID)) - continue - } - ctx := &types.Context{JSONReq: &request, HTTPReq: r} - args := []reflect.Value{reflect.ValueOf(ctx)} - if len(request.Params) > 0 { - fnArgs, err := jsonParamsToArgs(rpcFunc, request.Params) - if err != nil { - responses = append(responses, types.RPCInvalidParamsError(request.ID, errors.Wrap(err, "error converting json params to arguments"))) - continue - } - args = append(args, fnArgs...) - } - returns := rpcFunc.f.Call(args) - logger.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) - result, err := unreflectResult(returns) - if err != nil { - responses = append(responses, types.RPCInternalError(request.ID, err)) - continue + // --- Branch 2: Attempt to Unmarshal as a Single Request --- + var request types.RPCRequest + if err := json.Unmarshal(b, &request); err == nil { + if resp := processRequest(r, request, funcMap, logger); resp != nil { + WriteRPCResponseHTTP(w, *resp) + return } - responses = append(responses, types.NewRPCSuccessResponse(request.ID, result)) - } - if len(responses) == 0 { + } else { + WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request"))) return } + } +} - if isRPCRequestArray { - WriteRPCResponseArrayHTTP(w, responses) - return +// processRequest checks and processes a single JSON-RPC request. +// If the request should produce a response, it returns a pointer to that response. +// Otherwise (e.g. if the request is a notification or fails validation), it returns nil. +func processRequest(r *http.Request, req types.RPCRequest, funcMap map[string]*RPCFunc, logger *slog.Logger) *types.RPCResponse { + // Skip notifications (an empty ID indicates no response should be sent) + if req.ID == types.JSONRPCStringID("") { + logger.Debug("Skipping notification (empty ID)") + return nil + } + + // Check that the URL path is valid (assume only "/" is acceptable) + if len(r.URL.Path) > 1 { + resp := types.RPCInvalidRequestError(req.ID, fmt.Errorf("invalid path: %s", r.URL.Path)) + return &resp + } + + // Look up the requested method in the function map. + rpcFunc, ok := funcMap[req.Method] + if !ok || rpcFunc.ws { + resp := types.RPCMethodNotFoundError(req.ID) + return &resp + } + + ctx := &types.Context{JSONReq: &req, HTTPReq: r} + args := []reflect.Value{reflect.ValueOf(ctx)} + if len(req.Params) > 0 { + fnArgs, err := jsonParamsToArgs(rpcFunc, req.Params) + if err != nil { + resp := types.RPCInvalidParamsError(req.ID, errors.Wrap(err, "error converting json params to arguments")) + return &resp } + args = append(args, fnArgs...) + } - WriteRPCResponseHTTP(w, responses[0]) + // Call the RPC function using reflection. + returns := rpcFunc.f.Call(args) + logger.Info("HTTPJSONRPC", "method", req.Method, "args", args, "returns", returns) + + // Convert the reflection return values into a result value for JSON serialization. + result, err := unreflectResult(returns) + if err != nil { + resp := types.RPCInternalError(req.ID, err) + return &resp } + + // Build and return a successful response. + resp := types.NewRPCSuccessResponse(req.ID, result) + return &resp } func handleInvalidJSONRPCPaths(next http.HandlerFunc) http.HandlerFunc { From 07e3c4914ff9569ac765e0cb9ad19a7cc45fdfd5 Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 11 Feb 2025 15:46:37 +0100 Subject: [PATCH 58/60] refactor(gnovm): remove TRANS_BREAK from transcriber (#3626) was unused until now, and it looks redundant if we have TRANS_SKIP anyway. --- gnovm/pkg/gnolang/string_methods.go | 7 +- gnovm/pkg/gnolang/transcribe.go | 110 +++++++--------------------- 2 files changed, 28 insertions(+), 89 deletions(-) diff --git a/gnovm/pkg/gnolang/string_methods.go b/gnovm/pkg/gnolang/string_methods.go index 460e308fa9b..565b5860708 100644 --- a/gnovm/pkg/gnolang/string_methods.go +++ b/gnovm/pkg/gnolang/string_methods.go @@ -229,13 +229,12 @@ func _() { var x [1]struct{} _ = x[TRANS_CONTINUE-0] _ = x[TRANS_SKIP-1] - _ = x[TRANS_BREAK-2] - _ = x[TRANS_EXIT-3] + _ = x[TRANS_EXIT-2] } -const _TransCtrl_name = "TRANS_CONTINUETRANS_SKIPTRANS_BREAKTRANS_EXIT" +const _TransCtrl_name = "TRANS_CONTINUETRANS_SKIPTRANS_EXIT" -var _TransCtrl_index = [...]uint8{0, 14, 24, 35, 45} +var _TransCtrl_index = [...]uint8{0, 14, 24, 34} func (i TransCtrl) String() string { if i >= TransCtrl(len(_TransCtrl_index)-1) { diff --git a/gnovm/pkg/gnolang/transcribe.go b/gnovm/pkg/gnolang/transcribe.go index dab539a8707..572810e9668 100644 --- a/gnovm/pkg/gnolang/transcribe.go +++ b/gnovm/pkg/gnolang/transcribe.go @@ -14,7 +14,6 @@ type ( const ( TRANS_CONTINUE TransCtrl = iota TRANS_SKIP - TRANS_BREAK TRANS_EXIT ) @@ -101,7 +100,7 @@ const ( TRANS_IMPORT_PATH TRANS_CONST_TYPE TRANS_CONST_VALUE - TRANS_VAR_NAME // XXX stringer + TRANS_VAR_NAME TRANS_VAR_TYPE TRANS_VAR_VALUE TRANS_TYPE_TYPE @@ -113,8 +112,6 @@ const ( // - TRANS_SKIP to break out of the // ENTER,CHILDS1,[BLOCK,CHILDS2]?,LEAVE sequence for that node, // i.e. skipping (the rest of) it; -// - TRANS_BREAK to break out of looping in CHILDS1 or CHILDS2, -// but still perform TRANS_LEAVE. // - TRANS_EXIT to stop traversing altogether. // // Do not mutate ns. @@ -168,9 +165,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Args { cnn.Args[idx] = transcribe(t, nns, TRANS_CALL_ARG, idx, cnn.Args[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -248,16 +243,12 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc k, v := kvx.Key, kvx.Value if k != nil { k = transcribe(t, nns, TRANS_COMPOSITE_KEY, idx, k, &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } v = transcribe(t, nns, TRANS_COMPOSITE_VALUE, idx, v, &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } cnn.Elts[idx] = KeyValueExpr{Key: k, Value: v} @@ -269,9 +260,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.HeapCaptures { cnn.HeapCaptures[idx] = *(transcribe(t, nns, TRANS_FUNCLIT_HEAP_CAPTURE, idx, &cnn.HeapCaptures[idx], &c).(*NameExpr)) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -285,9 +274,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_FUNCLIT_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -321,9 +308,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc case *InterfaceTypeExpr: for idx := range cnn.Methods { cnn.Methods[idx] = *transcribe(t, nns, TRANS_INTERFACETYPE_METHOD, idx, &cnn.Methods[idx], &c).(*FieldTypeExpr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -341,9 +326,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Results { cnn.Results[idx] = *transcribe(t, nns, TRANS_FUNCTYPE_RESULT, idx, &cnn.Results[idx], &c).(*FieldTypeExpr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -359,9 +342,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc case *StructTypeExpr: for idx := range cnn.Fields { cnn.Fields[idx] = *transcribe(t, nns, TRANS_STRUCTTYPE_FIELD, idx, &cnn.Fields[idx], &c).(*FieldTypeExpr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -373,17 +354,13 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc case *AssignStmt: for idx := range cnn.Lhs { cnn.Lhs[idx] = transcribe(t, nns, TRANS_ASSIGN_LHS, idx, cnn.Lhs[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } for idx := range cnn.Rhs { cnn.Rhs[idx] = transcribe(t, nns, TRANS_ASSIGN_RHS, idx, cnn.Rhs[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -398,9 +375,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_BLOCK_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -409,9 +384,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_DECL_BODY, idx, cnn.Body[idx], &c).(SimpleDeclStmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -455,9 +428,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_FOR_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -506,9 +477,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_IF_CASE_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -544,18 +513,14 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_RANGE_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } case *ReturnStmt: for idx := range cnn.Results { cnn.Results[idx] = transcribe(t, nns, TRANS_RETURN_RESULT, idx, cnn.Results[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -564,9 +529,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc case *SelectStmt: for idx := range cnn.Cases { cnn.Cases[idx] = *transcribe(t, nns, TRANS_SELECT_CASE, idx, &cnn.Cases[idx], &c).(*SelectCaseStmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -585,9 +548,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_SELECTCASE_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -632,9 +593,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Clauses { cnn.Clauses[idx] = *transcribe(t, nns, TRANS_SWITCH_CASE, idx, &cnn.Clauses[idx], &c).(*SwitchClauseStmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -652,18 +611,14 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Cases { cnn.Cases[idx] = transcribe(t, nns, TRANS_SWITCHCASE_CASE, idx, cnn.Cases[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_SWITCHCASE_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -688,9 +643,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_FUNC_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -709,9 +662,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Values { cnn.Values[idx] = transcribe(t, nns, TRANS_VAR_VALUE, idx, cnn.Values[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -730,9 +681,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Decls { cnn.Decls[idx] = transcribe(t, nns, TRANS_FILE_BODY, idx, cnn.Decls[idx], &c).(Decl) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -768,12 +717,3 @@ func isStopOrSkip(oldnc *TransCtrl, nc TransCtrl) (stop bool) { panic("should not happen") } } - -// returns true if transcribe() should break (a loop). -func isBreak(nc TransCtrl) (brek bool) { - if nc == TRANS_BREAK { - return true - } else { - return false - } -} From 4f036699e4dc0024195fc142f8de60c72a34d66f Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 11 Feb 2025 16:22:10 +0100 Subject: [PATCH 59/60] chore(gnovm): instrument debug tracing to display the caller site. (#3702) Running the gnovm with the debug traces enabled (build tag `debug`) allows to display the details of preprocessing and opcodes operations. This PR adds the caller location in source (file + line) to each trace, allowing to identify the calling context of opcodes, to ease understanding the gnovm behavior. No change when debug is disabled (the default mode). Tracing can be activated by: `go run -tags debug ./cmd/gno run args` Before: ```console DEBUG: |||| -v (true bool) DEBUG: EXEC: (const (println func(xs ...interface{})()))((const ("i:" string)), i) DEBUG: |||| -s bodyStmt[0/0/1]=(end) DEBUG: |||| +o OpPopResults DEBUG: ||||| +x (const (println func(xs ...interface{})()))((const ("i:" string)), i) DEBUG: ||||| +o OpEval DEBUG: |||||| -o OpEval DEBUG: EVAL: (*gnolang.CallExpr) (const (println func(xs ...interface{})()))((const ("i:" string)), i) DEBUG: ||||| +o OpPrecall DEBUG: |||||| +x i DEBUG: |||||| +o OpEval DEBUG: ||||||| +x (const ("i:" string)) DEBUG: ||||||| +o OpEval DEBUG: |||||||| +x (const (println func(xs ...interface{})())) DEBUG: |||||||| +o OpEval DEBUG: ||||||||| -o OpEval DEBUG: EVAL: (*gnolang.ConstExpr) (const (println func(xs ...interface{})())) DEBUG: |||||||| -x (const (println func(xs ...interface{})())) DEBUG: |||||||| +v (println func(xs ...interface{})()) DEBUG: |||||||| -o OpEval DEBUG: EVAL: (*gnolang.ConstExpr) (const ("i:" string)) DEBUG: ||||||| -x (const ("i:" string)) DEBUG: ||||||| +v ("i:" string) DEBUG: ||||||| -o OpEval ``` After: ```console DEBUG: op_exec.go:99 : |||| -v (true bool) DEBUG: machine.go:1535: EXEC: (const (println func(xs ...interface{})()))((const ("i:" string)), i) DEBUG: op_exec.go:484 : |||| -s bodyStmt[0/0/1]=(end) DEBUG: op_exec.go:488 : |||| +o OpPopResults DEBUG: op_exec.go:493 : ||||| +x (const (println func(xs ...interface{})()))((const ("i:" string)), i) DEBUG: op_exec.go:494 : ||||| +o OpEval DEBUG: machine.go:1218: |||||| -o OpEval DEBUG: machine.go:1380: EVAL: (*gnolang.CallExpr) (const (println func(xs ...interface{})()))((const ("i:" string)), i) DEBUG: op_eval.go:243 : ||||| +o OpPrecall DEBUG: op_eval.go:247 : |||||| +x i DEBUG: op_eval.go:248 : |||||| +o OpEval DEBUG: op_eval.go:247 : ||||||| +x (const ("i:" string)) DEBUG: op_eval.go:248 : ||||||| +o OpEval DEBUG: op_eval.go:251 : |||||||| +x (const (println func(xs ...interface{})())) DEBUG: op_eval.go:252 : |||||||| +o OpEval DEBUG: machine.go:1218: ||||||||| -o OpEval DEBUG: machine.go:1380: EVAL: (*gnolang.ConstExpr) (const (println func(xs ...interface{})())) DEBUG: op_eval.go:317 : |||||||| -x (const (println func(xs ...interface{})())) DEBUG: op_eval.go:319 : |||||||| +v (println func(xs ...interface{})()) DEBUG: machine.go:1218: |||||||| -o OpEval DEBUG: machine.go:1380: EVAL: (*gnolang.ConstExpr) (const ("i:" string)) DEBUG: op_eval.go:317 : ||||||| -x (const ("i:" string)) DEBUG: op_eval.go:319 : ||||||| +v ("i:" string) DEBUG: machine.go:1218: ||||||| -o OpEval ``` --------- Co-authored-by: Morgan --- gnovm/pkg/gnolang/debug.go | 12 ++++++++++-- gnovm/pkg/gnolang/machine.go | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/gnovm/pkg/gnolang/debug.go b/gnovm/pkg/gnolang/debug.go index c7f9311ffe4..0f9cb9a1f9c 100644 --- a/gnovm/pkg/gnolang/debug.go +++ b/gnovm/pkg/gnolang/debug.go @@ -3,6 +3,8 @@ package gnolang import ( "fmt" "net/http" + "path" + "runtime" "strings" "time" @@ -48,7 +50,10 @@ var enabled bool = true func (debugging) Println(args ...interface{}) { if debug { if enabled { - fmt.Println(append([]interface{}{"DEBUG:"}, args...)...) + _, file, line, _ := runtime.Caller(2) + caller := fmt.Sprintf("%-.12s:%-4d", path.Base(file), line) + prefix := fmt.Sprintf("DEBUG: %17s: ", caller) + fmt.Println(append([]interface{}{prefix}, args...)...) } } } @@ -56,7 +61,10 @@ func (debugging) Println(args ...interface{}) { func (debugging) Printf(format string, args ...interface{}) { if debug { if enabled { - fmt.Printf("DEBUG: "+format, args...) + _, file, line, _ := runtime.Caller(2) + caller := fmt.Sprintf("%.12s:%-4d", path.Base(file), line) + prefix := fmt.Sprintf("DEBUG: %17s: ", caller) + fmt.Printf(prefix+format, args...) } } } diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 627854c9e9a..7b89e98eb6d 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -3,7 +3,9 @@ package gnolang import ( "fmt" "io" + "path" "reflect" + "runtime" "slices" "strconv" "strings" @@ -2120,8 +2122,11 @@ func (m *Machine) Panic(ex TypedValue) { func (m *Machine) Println(args ...interface{}) { if debug { if enabled { - s := strings.Repeat("|", m.NumOps) - debug.Println(append([]interface{}{s}, args...)...) + _, file, line, _ := runtime.Caller(2) // get caller info + caller := fmt.Sprintf("%-.12s:%-4d", path.Base(file), line) + prefix := fmt.Sprintf("DEBUG: %17s: ", caller) + s := prefix + strings.Repeat("|", m.NumOps) + fmt.Println(append([]interface{}{s}, args...)...) } } } @@ -2129,8 +2134,11 @@ func (m *Machine) Println(args ...interface{}) { func (m *Machine) Printf(format string, args ...interface{}) { if debug { if enabled { - s := strings.Repeat("|", m.NumOps) - debug.Printf(s+" "+format, args...) + _, file, line, _ := runtime.Caller(2) // get caller info + caller := fmt.Sprintf("%-.12s:%-4d", path.Base(file), line) + prefix := fmt.Sprintf("DEBUG: %17s: ", caller) + s := prefix + strings.Repeat("|", m.NumOps) + fmt.Printf(s+" "+format, args...) } } } From 7ca5ed2718988736cd1dc7ed493a053d8b6518fa Mon Sep 17 00:00:00 2001 From: Morgan Date: Tue, 11 Feb 2025 16:26:18 +0100 Subject: [PATCH 60/60] test(cmd/gno): don't fail if tests take more than 10 seconds (#3723) https://github.com/gnolang/gno/actions/runs/13265575679/job/37032064155 Changes the regex for txtar tests like `\d\.\d\ds` to have a + symbol instead, in case they take longer than 10 seconds. --- gnovm/cmd/gno/testdata/test/error_correct.txtar | 4 ++-- gnovm/cmd/gno/testdata/test/filetest_events.txtar | 6 +++--- gnovm/cmd/gno/testdata/test/minim2.txtar | 4 ++-- gnovm/cmd/gno/testdata/test/minim3.txtar | 4 ++-- gnovm/cmd/gno/testdata/test/multitest_events.txtar | 4 ++-- gnovm/cmd/gno/testdata/test/output_correct.txtar | 4 ++-- gnovm/cmd/gno/testdata/test/output_sync.txtar | 4 ++-- gnovm/cmd/gno/testdata/test/realm_correct.txtar | 6 +++--- gnovm/cmd/gno/testdata/test/realm_sync.txtar | 4 ++-- gnovm/cmd/gno/testdata/test/valid_filetest.txtar | 6 +++--- gnovm/cmd/gno/testdata/test/valid_test.txtar | 8 ++++---- 11 files changed, 27 insertions(+), 27 deletions(-) diff --git a/gnovm/cmd/gno/testdata/test/error_correct.txtar b/gnovm/cmd/gno/testdata/test/error_correct.txtar index f9ce4dd9028..bcd2c87da5c 100644 --- a/gnovm/cmd/gno/testdata/test/error_correct.txtar +++ b/gnovm/cmd/gno/testdata/test/error_correct.txtar @@ -3,8 +3,8 @@ gno test -v . stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/test/filetest_events.txtar b/gnovm/cmd/gno/testdata/test/filetest_events.txtar index 34da5fe2ff0..87f873980d5 100644 --- a/gnovm/cmd/gno/testdata/test/filetest_events.txtar +++ b/gnovm/cmd/gno/testdata/test/filetest_events.txtar @@ -3,14 +3,14 @@ gno test -print-events . ! stdout .+ -stderr 'ok \. \d\.\d\ds' +stderr 'ok \. \d+\.\d\ds' gno test -print-events -v . stdout 'test' stderr '=== RUN file/valid_filetest.gno' -stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/valid_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/test/minim2.txtar b/gnovm/cmd/gno/testdata/test/minim2.txtar index 3c4d1d085f0..d66d5076ef0 100644 --- a/gnovm/cmd/gno/testdata/test/minim2.txtar +++ b/gnovm/cmd/gno/testdata/test/minim2.txtar @@ -2,8 +2,8 @@ gno test . -! stdout .+ -stderr 'ok \. \d\.\d\ds' +! stdout .+ +stderr 'ok \. \d+\.\d\ds' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/test/minim3.txtar b/gnovm/cmd/gno/testdata/test/minim3.txtar index ac8ae0c41d4..ba1847a21df 100644 --- a/gnovm/cmd/gno/testdata/test/minim3.txtar +++ b/gnovm/cmd/gno/testdata/test/minim3.txtar @@ -2,8 +2,8 @@ gno test . -! stdout .+ -stderr 'ok \. \d\.\d\ds' +! stdout .+ +stderr 'ok \. \d+\.\d\ds' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/test/multitest_events.txtar b/gnovm/cmd/gno/testdata/test/multitest_events.txtar index 321c790561a..5cb134f46a1 100644 --- a/gnovm/cmd/gno/testdata/test/multitest_events.txtar +++ b/gnovm/cmd/gno/testdata/test/multitest_events.txtar @@ -2,10 +2,10 @@ gno test -print-events . -! stdout .+ +! stdout .+ stderr 'EVENTS: \[{\"type\":\"EventA\",\"attrs\":\[\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestA\"}\]' stderr 'EVENTS: \[{\"type\":\"EventB\",\"attrs\":\[{\"key\":\"keyA\",\"value\":\"valA\"}\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestB\"},{\"type\":\"EventC\",\"attrs\":\[{\"key\":\"keyD\",\"value\":\"valD\"}\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestB\"}\]' -stderr 'ok \. \d\.\d\ds' +stderr 'ok \. \d+\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/test/output_correct.txtar b/gnovm/cmd/gno/testdata/test/output_correct.txtar index a8aa878e0a4..3a829e66bee 100644 --- a/gnovm/cmd/gno/testdata/test/output_correct.txtar +++ b/gnovm/cmd/gno/testdata/test/output_correct.txtar @@ -5,8 +5,8 @@ gno test -v . stdout 'hey' stdout 'hru?' stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/test/output_sync.txtar b/gnovm/cmd/gno/testdata/test/output_sync.txtar index 45385a7eef9..1d701cc1c7f 100644 --- a/gnovm/cmd/gno/testdata/test/output_sync.txtar +++ b/gnovm/cmd/gno/testdata/test/output_sync.txtar @@ -6,8 +6,8 @@ stdout 'hey' stdout '^hru\?' stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden diff --git a/gnovm/cmd/gno/testdata/test/realm_correct.txtar b/gnovm/cmd/gno/testdata/test/realm_correct.txtar index ae1212133fd..8b1478d0df7 100644 --- a/gnovm/cmd/gno/testdata/test/realm_correct.txtar +++ b/gnovm/cmd/gno/testdata/test/realm_correct.txtar @@ -4,8 +4,8 @@ gno test -v . ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- x_filetest.gno -- // PKGPATH: gno.land/r/xx @@ -18,4 +18,4 @@ func main() { } // Realm: -// switchrealm["gno.land/r/xx"] \ No newline at end of file +// switchrealm["gno.land/r/xx"] diff --git a/gnovm/cmd/gno/testdata/test/realm_sync.txtar b/gnovm/cmd/gno/testdata/test/realm_sync.txtar index 65a930b2f03..91c83235d15 100644 --- a/gnovm/cmd/gno/testdata/test/realm_sync.txtar +++ b/gnovm/cmd/gno/testdata/test/realm_sync.txtar @@ -4,8 +4,8 @@ gno test -v . -update-golden-tests ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden diff --git a/gnovm/cmd/gno/testdata/test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar index 4e24ad9ab08..bd73ce3dc99 100644 --- a/gnovm/cmd/gno/testdata/test/valid_filetest.txtar +++ b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar @@ -3,14 +3,14 @@ gno test . ! stdout .+ -stderr 'ok \. \d\.\d\ds' +stderr 'ok \. \d+\.\d\ds' gno test -v . stdout 'test' stderr '=== RUN file/valid_filetest.gno' -stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/valid_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/test/valid_test.txtar b/gnovm/cmd/gno/testdata/test/valid_test.txtar index 9590626776c..5a9fe37a4b0 100644 --- a/gnovm/cmd/gno/testdata/test/valid_test.txtar +++ b/gnovm/cmd/gno/testdata/test/valid_test.txtar @@ -2,13 +2,13 @@ gno test . -! stdout .+ -stderr 'ok \. \d\.\d\ds' +! stdout .+ +stderr 'ok \. \d+\.\d\ds' gno test ./... -! stdout .+ -stderr 'ok \. \d\.\d\ds' +! stdout .+ +stderr 'ok \. \d+\.\d\ds' -- valid.gno -- package valid