diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 11f04ca8282..9de8d536b29 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -31,7 +31,7 @@ jobs: uses: coursier/cache-action@v6.4.6 - name: Set up JDK 17 - uses: coursier/setup-action@v1.3.5 + uses: coursier/setup-action@v1.3.6 with: jvm: temurin:1.17 diff --git a/contribs/gnodev/cmd/gnobro/main.go b/contribs/gnodev/cmd/gnobro/main.go index 6bb6bfc2396..092a441542a 100644 --- a/contribs/gnodev/cmd/gnobro/main.go +++ b/contribs/gnodev/cmd/gnobro/main.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "io" "log/slog" "net" "net/url" @@ -21,7 +22,6 @@ import ( "github.com/charmbracelet/wish" "github.com/charmbracelet/wish/activeterm" "github.com/charmbracelet/wish/bubbletea" - "github.com/charmbracelet/wish/logging" "golang.org/x/sync/errgroup" "github.com/gnolang/gno/contribs/gnodev/pkg/browser" @@ -47,6 +47,7 @@ type broCfg struct { sshListener string sshHostKeyPath string banner bool + jsonlog bool } var defaultBroOptions = broCfg{ @@ -152,6 +153,13 @@ func (c *broCfg) RegisterFlags(fs *flag.FlagSet) { defaultBroOptions.readonly, "readonly mode, no commands allowed", ) + + fs.BoolVar( + &c.jsonlog, + "jsonlog", + defaultBroOptions.jsonlog, + "display server log as json format", + ) } func execBrowser(cfg *broCfg, args []string, cio commands.IO) error { @@ -277,9 +285,7 @@ func runLocal(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg br func runServer(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg browser.Config, io commands.IO) error { // setup logger - charmlogger := charmlog.New(io.Out()) - charmlogger.SetLevel(charmlog.DebugLevel) - logger := slog.New(charmlogger) + logger := newLogger(io.Out(), cfg.jsonlog) teaHandler := func(s ssh.Session) (tea.Model, []tea.ProgramOption) { shortid := fmt.Sprintf("%.10s", s.Context().SessionID()) @@ -326,8 +332,8 @@ func runServer(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg b bubbletea.Middleware(teaHandler), activeterm.Middleware(), // ensure PTY ValidatePathCommandMiddleware(bcfg.URLPrefix), - logging.StructuredMiddlewareWithLogger( - charmlogger, charmlog.DebugLevel, + StructuredMiddlewareWithLogger( + ctx, logger, slog.LevelInfo, ), // XXX: add ip throttler ), @@ -358,7 +364,9 @@ func runServer(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg b return err } - io.Println("Bye!") + if !cfg.jsonlog { + io.Println("Bye!") + } return nil } @@ -460,3 +468,47 @@ func ValidatePathCommandMiddleware(pathPrefix string) wish.Middleware { } } } + +func StructuredMiddlewareWithLogger(ctx context.Context, logger *slog.Logger, level slog.Level) wish.Middleware { + return func(next ssh.Handler) ssh.Handler { + return func(sess ssh.Session) { + ct := time.Now() + hpk := sess.PublicKey() != nil + pty, _, _ := sess.Pty() + logger.Log( + ctx, + level, + "connect", + "user", sess.User(), + "remote-addr", sess.RemoteAddr().String(), + "public-key", hpk, + "command", sess.Command(), + "term", pty.Term, + "width", pty.Window.Width, + "height", pty.Window.Height, + "client-version", sess.Context().ClientVersion(), + ) + next(sess) + logger.Log( + ctx, + level, + "disconnect", + "user", sess.User(), + "remote-addr", sess.RemoteAddr().String(), + "duration", time.Since(ct), + ) + } + } +} + +func newLogger(out io.Writer, json bool) *slog.Logger { + if json { + return slog.New(slog.NewJSONHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + } + + charmlogger := charmlog.New(out) + charmlogger.SetLevel(charmlog.DebugLevel) + return slog.New(charmlogger) +} diff --git a/contribs/gnofaucet/README.md b/contribs/gnofaucet/README.md new file mode 100644 index 00000000000..eefa41a8c6f --- /dev/null +++ b/contribs/gnofaucet/README.md @@ -0,0 +1,25 @@ +# Start a local faucet + +## Step1: + +Make sure you have started gnoland + + ../../gno.land/build/gnoland start -lazy + +## Step2: + +Start the faucet. + + ./build/gnofaucet serve -chain-id dev -mnemonic "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" + +By default, the faucet sends out 10,000,000ugnot (10gnot) per request. + +## Step3: + +Make sure you have started website + + ../../gno.land/build/gnoweb + +Request testing tokens from following URL, Have fun! + + http://localhost:8888/faucet \ No newline at end of file diff --git a/docs/reference/gno-js-client/gno-provider.md b/docs/reference/gno-js-client/gno-provider.md index df808106cc3..1b9cbd53652 100644 --- a/docs/reference/gno-js-client/gno-provider.md +++ b/docs/reference/gno-js-client/gno-provider.md @@ -116,7 +116,7 @@ Returns **Promise** #### Usage ```ts -await provider.getFileContent('gno.land/r/demo/foo20', 'TotalSupply()') +await provider.getFileContent('gno.land/r/demo/foo20') /* foo20.gno foo20_test.gno diff --git a/examples/gno.land/p/demo/fqname/fqname.gno b/examples/gno.land/p/demo/fqname/fqname.gno new file mode 100644 index 00000000000..d28453e5c1b --- /dev/null +++ b/examples/gno.land/p/demo/fqname/fqname.gno @@ -0,0 +1,72 @@ +// Package fqname provides utilities for handling fully qualified identifiers in +// Gno. A fully qualified identifier typically includes a package path followed +// by a dot (.) and then the name of a variable, function, type, or other +// package-level declaration. +package fqname + +import "strings" + +// Parse splits a fully qualified identifier into its package path and name +// components. It handles cases with and without slashes in the package path. +// +// pkgpath, name := fqname.Parse("gno.land/p/demo/avl.Tree") +// ufmt.Sprintf("Package: %s, Name: %s\n", id.Package, id.Name) +// // Output: Package: gno.land/p/demo/avl, Name: Tree +func Parse(fqname string) (pkgpath, name string) { + // Find the index of the last slash. + lastSlashIndex := strings.LastIndex(fqname, "/") + if lastSlashIndex == -1 { + // No slash found, handle it as a simple package name with dot notation. + dotIndex := strings.LastIndex(fqname, ".") + if dotIndex == -1 { + return fqname, "" + } + return fqname[:dotIndex], fqname[dotIndex+1:] + } + + // Get the part after the last slash. + afterSlash := fqname[lastSlashIndex+1:] + + // Check for a dot in the substring after the last slash. + dotIndex := strings.Index(afterSlash, ".") + if dotIndex == -1 { + // No dot found after the last slash + return fqname, "" + } + + // Split at the dot to separate the base and the suffix. + base := fqname[:lastSlashIndex+1+dotIndex] + suffix := afterSlash[dotIndex+1:] + + return base, suffix +} + +// Construct a qualified identifier. +// +// fqName := fqname.Construct("gno.land/r/demo/foo20", "GRC20") +// fmt.Println("Fully Qualified Name:", fqName) +// // Output: gno.land/r/demo/foo20.GRC20 +func Construct(pkgpath, name string) string { + // TODO: ensure pkgpath is valid - and as such last part does not contain a dot. + if name == "" { + return pkgpath + } + return pkgpath + "." + name +} + +// RenderLink creates a formatted link for a fully qualified identifier. +// If the package path starts with "gno.land", it converts it to a markdown link. +// If the domain is different or missing, it returns the input as is. +func RenderLink(pkgPath, slug string) string { + if strings.HasPrefix(pkgPath, "gno.land") { + pkgLink := strings.TrimPrefix(pkgPath, "gno.land") + if slug != "" { + return "[" + pkgPath + "](" + pkgLink + ")." + slug + } + return "[" + pkgPath + "](" + pkgLink + ")" + } + if slug != "" { + return pkgPath + "." + slug + } + return pkgPath +} diff --git a/examples/gno.land/p/demo/fqname/fqname_test.gno b/examples/gno.land/p/demo/fqname/fqname_test.gno new file mode 100644 index 00000000000..55a220776be --- /dev/null +++ b/examples/gno.land/p/demo/fqname/fqname_test.gno @@ -0,0 +1,74 @@ +package fqname + +import ( + "testing" + + "gno.land/p/demo/uassert" +) + +func TestParse(t *testing.T) { + tests := []struct { + input string + expectedPkgPath string + expectedName string + }{ + {"gno.land/p/demo/avl.Tree", "gno.land/p/demo/avl", "Tree"}, + {"gno.land/p/demo/avl", "gno.land/p/demo/avl", ""}, + {"gno.land/p/demo/avl.Tree.Node", "gno.land/p/demo/avl", "Tree.Node"}, + {"gno.land/p/demo/avl/nested.Package.Func", "gno.land/p/demo/avl/nested", "Package.Func"}, + {"path/filepath.Split", "path/filepath", "Split"}, + {"path.Split", "path", "Split"}, + {"path/filepath", "path/filepath", ""}, + {"path", "path", ""}, + {"", "", ""}, + } + + for _, tt := range tests { + pkgpath, name := Parse(tt.input) + uassert.Equal(t, tt.expectedPkgPath, pkgpath, "Package path did not match") + uassert.Equal(t, tt.expectedName, name, "Name did not match") + } +} + +func TestConstruct(t *testing.T) { + tests := []struct { + pkgpath string + name string + expected string + }{ + {"gno.land/r/demo/foo20", "GRC20", "gno.land/r/demo/foo20.GRC20"}, + {"gno.land/r/demo/foo20", "", "gno.land/r/demo/foo20"}, + {"path", "", "path"}, + {"path", "Split", "path.Split"}, + {"path/filepath", "", "path/filepath"}, + {"path/filepath", "Split", "path/filepath.Split"}, + {"", "JustName", ".JustName"}, + {"", "", ""}, + } + + for _, tt := range tests { + result := Construct(tt.pkgpath, tt.name) + uassert.Equal(t, tt.expected, result, "Constructed FQName did not match expected") + } +} + +func TestRenderLink(t *testing.T) { + tests := []struct { + pkgPath string + slug string + expected string + }{ + {"gno.land/p/demo/avl", "Tree", "[gno.land/p/demo/avl](/p/demo/avl).Tree"}, + {"gno.land/p/demo/avl", "", "[gno.land/p/demo/avl](/p/demo/avl)"}, + {"github.com/a/b", "C", "github.com/a/b.C"}, + {"example.com/pkg", "Func", "example.com/pkg.Func"}, + {"gno.land/r/demo/foo20", "GRC20", "[gno.land/r/demo/foo20](/r/demo/foo20).GRC20"}, + {"gno.land/r/demo/foo20", "", "[gno.land/r/demo/foo20](/r/demo/foo20)"}, + {"", "", ""}, + } + + for _, tt := range tests { + result := RenderLink(tt.pkgPath, tt.slug) + uassert.Equal(t, tt.expected, result, "Rendered link did not match expected") + } +} diff --git a/examples/gno.land/p/demo/fqname/gno.mod b/examples/gno.land/p/demo/fqname/gno.mod new file mode 100644 index 00000000000..1282e262303 --- /dev/null +++ b/examples/gno.land/p/demo/fqname/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/demo/fqname + +require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/moul/printfdebugging/color.gno b/examples/gno.land/p/moul/printfdebugging/color.gno new file mode 100644 index 00000000000..b3bf647b9b5 --- /dev/null +++ b/examples/gno.land/p/moul/printfdebugging/color.gno @@ -0,0 +1,81 @@ +package printfdebugging + +// consts copied from https://github.com/fatih/color/blob/main/color.go + +// Attribute defines a single SGR Code +type Attribute int + +const Escape = "\x1b" + +// Base attributes +const ( + Reset Attribute = iota + Bold + Faint + Italic + Underline + BlinkSlow + BlinkRapid + ReverseVideo + Concealed + CrossedOut +) + +const ( + ResetBold Attribute = iota + 22 + ResetItalic + ResetUnderline + ResetBlinking + _ + ResetReversed + ResetConcealed + ResetCrossedOut +) + +// Foreground text colors +const ( + FgBlack Attribute = iota + 30 + FgRed + FgGreen + FgYellow + FgBlue + FgMagenta + FgCyan + FgWhite +) + +// Foreground Hi-Intensity text colors +const ( + FgHiBlack Attribute = iota + 90 + FgHiRed + FgHiGreen + FgHiYellow + FgHiBlue + FgHiMagenta + FgHiCyan + FgHiWhite +) + +// Background text colors +const ( + BgBlack Attribute = iota + 40 + BgRed + BgGreen + BgYellow + BgBlue + BgMagenta + BgCyan + BgWhite +) + +// Background Hi-Intensity text colors +const ( + BgHiBlack Attribute = iota + 100 + BgHiRed + BgHiGreen + BgHiYellow + BgHiBlue + BgHiMagenta + BgHiCyan + BgHiWhite +) diff --git a/examples/gno.land/p/moul/printfdebugging/gno.mod b/examples/gno.land/p/moul/printfdebugging/gno.mod new file mode 100644 index 00000000000..2cf6aa09e61 --- /dev/null +++ b/examples/gno.land/p/moul/printfdebugging/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/demo/printfdebugging + +require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/moul/printfdebugging/printfdebugging.gno b/examples/gno.land/p/moul/printfdebugging/printfdebugging.gno new file mode 100644 index 00000000000..a12a3dfadd2 --- /dev/null +++ b/examples/gno.land/p/moul/printfdebugging/printfdebugging.gno @@ -0,0 +1,19 @@ +// this package is a joke... or not. +package printfdebugging + +import ( + "strings" + + "gno.land/p/demo/ufmt" +) + +func BigRedLine(args ...string) { + println(ufmt.Sprintf("%s[%dm####################################%s[%dm %s", + Escape, int(BgRed), Escape, int(Reset), + strings.Join(args, " "), + )) +} + +func Success() { + println(" \033[31mS\033[33mU\033[32mC\033[36mC\033[34mE\033[35mS\033[31mS\033[0m ") +} diff --git a/examples/gno.land/r/demo/boards/README.md b/examples/gno.land/r/demo/boards/README.md index a9b68ec9c92..628bc9aa349 100644 --- a/examples/gno.land/r/demo/boards/README.md +++ b/examples/gno.land/r/demo/boards/README.md @@ -58,7 +58,8 @@ your `ACCOUNT_ADDR` and `KEYNAME` Instead of editing `gno.land/genesis/genesis_balances.txt`, a more general solution (with more steps) is to run a local "faucet" and use the web browser to add $GNOT. (This can be done at any time.) -See this page: https://github.com/gnolang/gno/blob/master/gno.land/cmd/gnofaucet/README.md +See this page: https://github.com/gnolang/gno/blob/master/contribs/gnofaucet/README.md + ### Start the `gnoland` node. diff --git a/examples/gno.land/r/demo/counter/counter.gno b/examples/gno.land/r/demo/counter/counter.gno new file mode 100644 index 00000000000..43943e114dc --- /dev/null +++ b/examples/gno.land/r/demo/counter/counter.gno @@ -0,0 +1,14 @@ +package counter + +import "strconv" + +var counter int + +func Increment() int { + counter++ + return counter +} + +func Render(_ string) string { + return strconv.Itoa(counter) +} diff --git a/examples/gno.land/r/demo/counter/counter_test.gno b/examples/gno.land/r/demo/counter/counter_test.gno new file mode 100644 index 00000000000..352889f7e59 --- /dev/null +++ b/examples/gno.land/r/demo/counter/counter_test.gno @@ -0,0 +1,22 @@ +package counter + +import "testing" + +func TestIncrement(t *testing.T) { + counter = 0 + val := Increment() + if val != 1 { + t.Fatalf("result from Increment(): %d != 1", val) + } + if counter != val { + t.Fatalf("counter (%d) != val (%d)", counter, val) + } +} + +func TestRender(t *testing.T) { + counter = 1337 + res := Render("") + if res != "1337" { + t.Fatalf("render result %q != %q", res, "1337") + } +} diff --git a/examples/gno.land/r/demo/counter/gno.mod b/examples/gno.land/r/demo/counter/gno.mod new file mode 100644 index 00000000000..332d4e6da6a --- /dev/null +++ b/examples/gno.land/r/demo/counter/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/counter diff --git a/tm2/pkg/bft/fail/fail.go b/tm2/pkg/bft/fail/fail.go index 607084f484f..c56f43d7d89 100644 --- a/tm2/pkg/bft/fail/fail.go +++ b/tm2/pkg/bft/fail/fail.go @@ -4,31 +4,33 @@ import ( "fmt" "os" "strconv" + "sync" ) -func envSet() int { +func setFromEnv() { callIndexToFailS := os.Getenv("FAIL_TEST_INDEX") if callIndexToFailS == "" { - return -1 + callIndexToFail = -1 } else { var err error - callIndexToFail, err := strconv.Atoi(callIndexToFailS) + callIndexToFail, err = strconv.Atoi(callIndexToFailS) if err != nil { - return -1 + callIndexToFail = -1 } - return callIndexToFail } } -// Fail when FAIL_TEST_INDEX == callIndex -var callIndex int // indexes Fail calls +var ( + callIndex int // indexes Fail calls + callIndexToFail int // index of call which should fail + callIndexToFailOnce sync.Once // sync.Once to set the value of the above +) +// Fail exits the program when after being called the same number of times as +// that passed as the FAIL_TEST_INDEX environment variable. func Fail() { - callIndexToFail := envSet() - if callIndexToFail < 0 { - return - } + callIndexToFailOnce.Do(setFromEnv) if callIndex == callIndexToFail { Exit() diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 867a38d680a..671f18cf058 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -625,11 +625,12 @@ func (app *BaseApp) runMsgs(ctx Context, msgs []Msg, mode RunTxMode) (result Res ctx = ctx.WithEventLogger(NewEventLogger()) msgLogs := make([]string, 0, len(msgs)) - data := make([]byte, 0, len(msgs)) - err := error(nil) - events := []Event{} + var ( + err error + events = []Event{} + ) // NOTE: GasWanted is determined by ante handler and GasUsed by the GasMeter. for i, msg := range msgs { @@ -660,6 +661,7 @@ func (app *BaseApp) runMsgs(ctx Context, msgs []Msg, mode RunTxMode) (result Res fmt.Sprintf("msg:%d,success:%v,log:%s,events:%v", i, false, msgResult.Log, events)) err = msgResult.Error + events = nil break } @@ -667,7 +669,10 @@ func (app *BaseApp) runMsgs(ctx Context, msgs []Msg, mode RunTxMode) (result Res fmt.Sprintf("msg:%d,success:%v,log:%s,events:%v", i, true, msgResult.Log, events)) } - events = append(events, ctx.EventLogger().Events()...) + + if err == nil { + events = append(events, ctx.EventLogger().Events()...) + } result.Error = ABCIError(err) result.Data = data