From eabc5c40d31a00adf8cf97e6460b40704e0dba1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Wed, 1 Mar 2023 14:53:51 +0100 Subject: [PATCH] feat: utilize ffcli (CLI refactor) (#497) --- .github/workflows/examples.yml | 4 +- Makefile | 8 +- cmd/genproto/genproto.go | 25 + cmd/gnodev/build.go | 67 ++- cmd/gnodev/build_test.go | 8 +- cmd/gnodev/main.go | 126 ++--- cmd/gnodev/main_test.go | 30 +- cmd/gnodev/mod.go | 33 +- cmd/gnodev/mod_test.go | 9 +- cmd/gnodev/precompile.go | 123 +++-- cmd/gnodev/precompile_test.go | 8 +- cmd/gnodev/repl.go | 63 ++- cmd/gnodev/repl_test.go | 6 +- cmd/gnodev/run.go | 78 ++-- cmd/gnodev/run_test.go | 20 +- cmd/gnodev/test.go | 206 ++++---- cmd/gnodev/test_test.go | 71 ++- cmd/gnofaucet/README.md | 18 +- cmd/gnofaucet/main.go | 30 ++ cmd/gnofaucet/{gnofaucet.go => serve.go} | 262 +++++++---- cmd/gnokey/gnokeys.go | 441 ------------------ cmd/gnokey/main.go | 19 + cmd/gnoland/main.go | 138 ++++-- cmd/gnoland/main_test.go | 17 +- cmd/gnotxport/export.go | 115 +++-- cmd/gnotxport/import.go | 127 +++-- cmd/gnotxport/main.go | 72 +-- cmd/goscan/goscan.go | 33 +- go.mod | 1 + go.sum | 4 +- pkgs/command/app.go | 11 - pkgs/command/command.go | 153 ------ pkgs/command/flags.go | 417 ----------------- pkgs/command/flags_test.go | 23 - pkgs/command/prompt.go | 153 ------ pkgs/commands/command.go | 83 ++++ pkgs/commands/empty.go | 16 + pkgs/commands/io.go | 127 +++++ pkgs/commands/types.go | 24 + pkgs/commands/utils.go | 115 +++++ pkgs/crypto/keys/client/add.go | 201 +++++--- pkgs/crypto/keys/client/add_ledger_test.go | 107 ----- pkgs/crypto/keys/client/add_test.go | 89 ++-- pkgs/crypto/keys/client/addpkg.go | 217 +++++++++ pkgs/crypto/keys/client/broadcast.go | 71 ++- pkgs/crypto/keys/client/call.go | 142 ++++++ .../keys/client/{options.go => common.go} | 8 +- pkgs/crypto/keys/client/consts.go | 5 - pkgs/crypto/keys/client/consts_test.go | 6 - pkgs/crypto/keys/client/delete.go | 79 +++- pkgs/crypto/keys/client/delete_test.go | 56 ++- pkgs/crypto/keys/client/export.go | 98 ++-- pkgs/crypto/keys/client/export_test.go | 34 +- pkgs/crypto/keys/client/generate.go | 57 ++- pkgs/crypto/keys/client/generate_test.go | 50 +- pkgs/crypto/keys/client/import.go | 100 ++-- pkgs/crypto/keys/client/import_test.go | 23 +- pkgs/crypto/keys/client/list.go | 39 +- pkgs/crypto/keys/client/list_test.go | 14 +- pkgs/crypto/keys/client/maketx.go | 79 ++++ pkgs/crypto/keys/client/query.go | 76 ++- pkgs/crypto/keys/client/root.go | 108 +++-- pkgs/crypto/keys/client/send.go | 130 ++++++ pkgs/crypto/keys/client/sign.go | 148 ++++-- pkgs/crypto/keys/client/sign_test.go | 54 ++- pkgs/crypto/keys/client/verify.go | 62 ++- pkgs/crypto/keys/client/verify_test.go | 42 +- 67 files changed, 2923 insertions(+), 2456 deletions(-) create mode 100644 cmd/gnofaucet/main.go rename cmd/gnofaucet/{gnofaucet.go => serve.go} (64%) delete mode 100644 cmd/gnokey/gnokeys.go create mode 100644 cmd/gnokey/main.go delete mode 100644 pkgs/command/app.go delete mode 100644 pkgs/command/command.go delete mode 100644 pkgs/command/flags.go delete mode 100644 pkgs/command/flags_test.go delete mode 100644 pkgs/command/prompt.go create mode 100644 pkgs/commands/command.go create mode 100644 pkgs/commands/empty.go create mode 100644 pkgs/commands/io.go create mode 100644 pkgs/commands/types.go create mode 100644 pkgs/commands/utils.go delete mode 100644 pkgs/crypto/keys/client/add_ledger_test.go create mode 100644 pkgs/crypto/keys/client/addpkg.go create mode 100644 pkgs/crypto/keys/client/call.go rename pkgs/crypto/keys/client/{options.go => common.go} (60%) delete mode 100644 pkgs/crypto/keys/client/consts.go delete mode 100644 pkgs/crypto/keys/client/consts_test.go create mode 100644 pkgs/crypto/keys/client/maketx.go create mode 100644 pkgs/crypto/keys/client/send.go diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 251b50966c4..340c858f904 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -31,5 +31,5 @@ jobs: # TODO: implement --allow-all-imports #- run: gnodev precompile ./stdlibs --verbose --allow-all-imports #- run: gnodev precompile ./tests --verbose --allow-all-imports - - run: go run ./cmd/gnodev precompile ./examples --verbose - - run: go run ./cmd/gnodev build ./examples --verbose + - run: go run ./cmd/gnodev precompile --verbose ./examples + - run: go run ./cmd/gnodev build --verbose ./examples diff --git a/Makefile b/Makefile index 5ef500b9969..5f3b8727143 100644 --- a/Makefile +++ b/Makefile @@ -65,10 +65,10 @@ clean: rm -rf build examples.precompile: install_gnodev - go run ./cmd/gnodev precompile ./examples --verbose + go run ./cmd/gnodev precompile --verbose ./examples examples.build: install_gnodev examples.precompile - go run ./cmd/gnodev build ./examples --verbose + go run ./cmd/gnodev build --verbose ./examples ######################################## # Formatting, linting. @@ -146,10 +146,10 @@ test.packages2: go test tests/*.go -v -run "TestPackages/bytes" --timeout 30m test.examples: - go run ./cmd/gnodev test ./examples --verbose + go run ./cmd/gnodev test --verbose ./examples test.examples.sync: - go run ./cmd/gnodev test ./examples --verbose --update-golden-tests + go run ./cmd/gnodev test --verbose --update-golden-tests ./examples # Code gen stringer: diff --git a/cmd/genproto/genproto.go b/cmd/genproto/genproto.go index f6d087c6159..332b69ddf2c 100644 --- a/cmd/genproto/genproto.go +++ b/cmd/genproto/genproto.go @@ -1,8 +1,13 @@ package main import ( + "context" + "fmt" + "os" + "github.com/gnolang/gno/pkgs/amino" "github.com/gnolang/gno/pkgs/amino/genproto" + "github.com/gnolang/gno/pkgs/commands" // TODO: move these out. abci "github.com/gnolang/gno/pkgs/bft/abci/types" @@ -24,6 +29,22 @@ import ( ) func main() { + cmd := commands.NewCommand( + commands.Metadata{ + LongHelp: "Generates proto bindings for Amino packages", + }, + commands.NewEmptyConfig(), + execGen, + ) + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v", err) + + os.Exit(1) + } +} + +func execGen(_ context.Context, _ []string) error { pkgs := []*amino.Package{ bitarray.Package, merkle.Package, @@ -42,12 +63,16 @@ func main() { vm.Package, gno.Package, } + for _, pkg := range pkgs { genproto.WriteProto3Schema(pkg) genproto.WriteProtoBindings(pkg) genproto.MakeProtoFolder(pkg, "proto") } + for _, pkg := range pkgs { genproto.RunProtoc(pkg, "proto") } + + return nil } diff --git a/cmd/gnodev/build.go b/cmd/gnodev/build.go index 24dfb0c00a2..ae0423ad4d5 100644 --- a/cmd/gnodev/build.go +++ b/cmd/gnodev/build.go @@ -1,29 +1,60 @@ package main import ( + "context" + "flag" "fmt" "os" - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/commands" gno "github.com/gnolang/gno/pkgs/gnolang" ) -type buildOptions struct { - Verbose bool `flag:"verbose" help:"verbose"` - GoBinary string `flag:"go-binary" help:"go binary to use for building"` +type buildCfg struct { + verbose bool + goBinary string } -var defaultBuildOptions = buildOptions{ - Verbose: false, - GoBinary: "go", +var defaultBuildOptions = &buildCfg{ + verbose: false, + goBinary: "go", } -func buildApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(buildOptions) +func newBuildCmd(io *commands.IO) *commands.Command { + cfg := &buildCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "build", + ShortUsage: "build [flags] ", + ShortHelp: "Builds the specified gno package", + }, + cfg, + func(_ context.Context, args []string) error { + return execBuild(cfg, args, io) + }, + ) +} + +func (c *buildCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.verbose, + "verbose", + defaultBuildOptions.verbose, + "verbose output when building", + ) + + fs.StringVar( + &c.goBinary, + "go-binary", + defaultBuildOptions.goBinary, + "go binary to use for building", + ) +} + +func execBuild(cfg *buildCfg, args []string, io *commands.IO) error { if len(args) < 1 { - cmd.ErrPrintfln("Usage: build [build flags] [packages]") - return errors.New("invalid args") + return flag.ErrHelp } paths, err := gnoPackagesFromArgs(args) @@ -33,13 +64,15 @@ func buildApp(cmd *command.Command, args []string, iopts interface{}) error { errCount := 0 for _, pkgPath := range paths { - err = goBuildFileOrPkg(pkgPath, opts) + err = goBuildFileOrPkg(pkgPath, cfg) if err != nil { err = fmt.Errorf("%s: build pkg: %w", pkgPath, err) - cmd.ErrPrintfln("%s", err.Error()) + io.ErrPrintfln("%s\n", err.Error()) + errCount++ } } + if errCount > 0 { return fmt.Errorf("%d go build errors", errCount) } @@ -47,9 +80,9 @@ func buildApp(cmd *command.Command, args []string, iopts interface{}) error { return nil } -func goBuildFileOrPkg(fileOrPkg string, opts buildOptions) error { - verbose := opts.Verbose - goBinary := opts.GoBinary +func goBuildFileOrPkg(fileOrPkg string, cfg *buildCfg) error { + verbose := cfg.verbose + goBinary := cfg.goBinary if verbose { fmt.Fprintf(os.Stderr, "%s\n", fileOrPkg) diff --git a/cmd/gnodev/build_test.go b/cmd/gnodev/build_test.go index 23caa8f0080..89339ee8a6e 100644 --- a/cmd/gnodev/build_test.go +++ b/cmd/gnodev/build_test.go @@ -5,12 +5,8 @@ import "testing" func TestBuildApp(t *testing.T) { tc := []testMainCase{ { - args: []string{"build"}, - errShouldBe: "invalid args", - stderrShouldBe: "Usage: build [build flags] [packages]\n", - }, { - args: []string{"build", "--help"}, - stdoutShouldContain: "# buildOptions options\n-", + args: []string{"build"}, + errShouldBe: "flag: help requested", }, // {args: []string{"build", "..."}, stdoutShouldContain: "..."}, diff --git a/cmd/gnodev/main.go b/cmd/gnodev/main.go index 31823e73659..7405f281e39 100644 --- a/cmd/gnodev/main.go +++ b/cmd/gnodev/main.go @@ -1,99 +1,53 @@ package main import ( + "context" + "fmt" "os" - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/commands" ) func main() { - cmd := command.NewStdCommand() - exec := os.Args[0] - args := os.Args[1:] - err := runMain(cmd, exec, args) - if err != nil { - cmd.ErrPrintfln("%s", err.Error()) - // cmd.ErrPrintfln("%#v", err) - os.Exit(1) - } -} + cmd := newGnodevCmd(commands.NewDefaultIO()) -type ( - AppItem = command.AppItem - AppList = command.AppList -) - -var mainApps AppList = []AppItem{ - { - App: runApp, - Name: "run", - Desc: "run a Gno file", - Defaults: defaultRunOptions, - }, - { - App: buildApp, - Name: "build", - Desc: "build a gno package", - Defaults: defaultBuildOptions, - }, - { - App: modApp, - Name: "mod", - Desc: "manage gno.mod", - Defaults: defaultModFlags, - }, - { - App: precompileApp, - Name: "precompile", - Desc: "precompile .gno to .go", - Defaults: defaultPrecompileFlags, - }, - { - App: testApp, - Name: "test", - Desc: "test a gno package", - Defaults: defaultTestOptions, - }, - { - App: replApp, - Name: "repl", - Desc: "start a GnoVM REPL", - Defaults: defaultReplOptions, - }, + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v", err) - // fmt -- gofmt - // clean - // graph - // vendor -- download deps from the chain in vendor/ - // list -- list packages - // render -- call render()? - // publish/release - // generate - // doc -- godoc - // "vm" -- starts an in-memory chain that can be interacted with? - // bug -- start a bug report - // version -- show gnodev, golang versions -} - -func runMain(cmd *command.Command, exec string, args []string) error { - // show help message. - if len(args) == 0 || args[0] == "help" || args[0] == "--help" { - cmd.Println("available subcommands:") - for _, appItem := range mainApps { - cmd.Printf(" %s - %s\n", appItem.Name, appItem.Desc) - } - return nil - } - - // switch on first argument. - for _, appItem := range mainApps { - if appItem.Name == args[0] { - err := cmd.Run(appItem.App, args[1:], appItem.Defaults) - return err // done - } + os.Exit(1) } +} - // unknown app command! - return errors.New("unknown command " + args[0]) +func newGnodevCmd(io *commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + LongHelp: "Runs the gno development toolkit", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newRunCmd(io), + newBuildCmd(io), + newPrecompileCmd(io), + newTestCmd(io), + newReplCmd(), + newModCmd(), + // fmt -- gofmt + // clean + // graph + // vendor -- download deps from the chain in vendor/ + // list -- list packages + // render -- call render()? + // publish/release + // generate + // doc -- godoc + // "vm" -- starts an in-memory chain that can be interacted with? + // bug -- start a bug report + // version -- show gnodev, golang versions + ) + + return cmd } diff --git a/cmd/gnodev/main_test.go b/cmd/gnodev/main_test.go index 9eb31a34243..14cdb174a16 100644 --- a/cmd/gnodev/main_test.go +++ b/cmd/gnodev/main_test.go @@ -2,20 +2,22 @@ package main import ( "bytes" + "context" "fmt" "os" "path/filepath" "strings" "testing" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/stretchr/testify/require" ) -func TestMain(t *testing.T) { +func TestMain_Gnodev(t *testing.T) { tc := []testMainCase{ - {args: []string{""}, errShouldBe: "unknown command "}, + {args: []string{""}, errShouldBe: "flag: help requested"}, } + testMainCaseRun(t, tc) } @@ -52,15 +54,8 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { testName = strings.ReplaceAll(testName+test.testDir, "/", "~") t.Run(testName, func(t *testing.T) { - cmd := command.NewMockCommand() mockOut := bytes.NewBufferString("") mockErr := bytes.NewBufferString("") - stdout := command.WriteNopCloser(mockOut) - stderr := command.WriteNopCloser(mockErr) - cmd.SetOut(stdout) - cmd.SetErr(stderr) - - require.NotNil(t, cmd) checkOutputs := func(t *testing.T) { t.Helper() @@ -73,7 +68,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { require.Contains(t, mockOut.String(), test.stdoutShouldContain, "stdout should contain") } if test.stdoutShouldBe != "" { - require.Equal(t, mockOut.String(), test.stdoutShouldBe, "stdout should be") + require.Equal(t, test.stdoutShouldBe, mockOut.String(), "stdout should be") } } @@ -85,12 +80,11 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { require.Contains(t, mockErr.String(), test.stderrShouldContain, "stderr should contain") } if test.stderrShouldBe != "" { - require.Equal(t, mockErr.String(), test.stderrShouldBe, "stderr should be") + require.Equal(t, test.stderrShouldBe, mockErr.String(), "stderr should be") } } } - exec := "gnodev" defer func() { if r := recover(); r != nil { output := fmt.Sprintf("%v", r) @@ -101,7 +95,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { require.Contains(t, output, test.recoverShouldContain, "recover should contain") } if test.recoverShouldBe != "" { - require.Equal(t, output, test.recoverShouldBe, "recover should be") + require.Equal(t, test.recoverShouldBe, output, "recover should be") } checkOutputs(t) } else { @@ -124,7 +118,11 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { defer os.Chdir(workingDir) } - err := runMain(cmd, exec, test.args) + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOut)) + io.SetErr(commands.WriteNopCloser(mockErr)) + + err := newGnodevCmd(io).ParseAndRun(context.Background(), test.args) if errShouldBeEmpty { require.Nil(t, err, "err should be nil") @@ -135,7 +133,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { require.Contains(t, err.Error(), test.errShouldContain, "err should contain") } if test.errShouldBe != "" { - require.Equal(t, err.Error(), test.errShouldBe, "err should be") + require.Equal(t, test.errShouldBe, err.Error(), "err should be") } } diff --git a/cmd/gnodev/mod.go b/cmd/gnodev/mod.go index 2e75f72d340..ec4b7b88cf3 100644 --- a/cmd/gnodev/mod.go +++ b/cmd/gnodev/mod.go @@ -1,34 +1,39 @@ package main import ( + "context" + "flag" "fmt" "os" "path/filepath" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/errors" "github.com/gnolang/gno/pkgs/gnolang/gnomod" ) -type modFlags struct { - Verbose bool `flag:"verbose" help:"verbose"` +func newModCmd() *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "mod", + ShortUsage: "mod ", + ShortHelp: "Manage gno.mod", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execMod(args) + }, + ) } -var defaultModFlags = modFlags{ - Verbose: false, -} - -func modApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(modFlags) - +func execMod(args []string) error { if len(args) != 1 { - cmd.ErrPrintfln("Usage: mod [flags] ") - return errors.New("invalid command") + return flag.ErrHelp } switch args[0] { case "download": - if err := runModDownload(&opts); err != nil { + if err := runModDownload(); err != nil { return fmt.Errorf("mod download: %w", err) } default: @@ -38,7 +43,7 @@ func modApp(cmd *command.Command, args []string, iopts interface{}) error { return nil } -func runModDownload(opts *modFlags) error { +func runModDownload() error { path, err := os.Getwd() if err != nil { return err diff --git a/cmd/gnodev/mod_test.go b/cmd/gnodev/mod_test.go index a431d70bfdc..4238b0087b8 100644 --- a/cmd/gnodev/mod_test.go +++ b/cmd/gnodev/mod_test.go @@ -5,13 +5,8 @@ import "testing" func TestModApp(t *testing.T) { tc := []testMainCase{ { - args: []string{"mod"}, - errShouldBe: "invalid command", - stderrShouldBe: "Usage: mod [flags] \n", - }, - { - args: []string{"mod", "--help"}, - stdoutShouldContain: "# modFlags options\n-", + args: []string{"mod"}, + errShouldBe: "flag: help requested", }, // test gno.mod diff --git a/cmd/gnodev/precompile.go b/cmd/gnodev/precompile.go index 1d349fbc1d4..12a4458c0eb 100644 --- a/cmd/gnodev/precompile.go +++ b/cmd/gnodev/precompile.go @@ -1,49 +1,41 @@ package main import ( + "context" + "flag" "fmt" "log" "os" "path/filepath" - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/commands" gno "github.com/gnolang/gno/pkgs/gnolang" ) type importPath string -type precompileFlags struct { - Verbose bool `flag:"verbose" help:"verbose"` - SkipFmt bool `flag:"skip-fmt" help:"do not check syntax of generated .go files"` - SkipImports bool `flag:"skip-imports" help:"do not precompile imports recursively"` - GoBinary string `flag:"go-binary" help:"go binary to use for building"` - GofmtBinary string `flag:"go-binary" help:"gofmt binary to use for syntax checking"` - Output string `flag:"output" help:"output directory"` -} - -var defaultPrecompileFlags = precompileFlags{ - Verbose: false, - SkipFmt: false, - SkipImports: false, - GoBinary: "go", - GofmtBinary: "gofmt", - Output: ".", +type precompileCfg struct { + verbose bool + skipFmt bool + skipImports bool + goBinary string + gofmtBinary string + output string } type precompileOptions struct { - flags precompileFlags + cfg *precompileCfg // precompiled is the set of packages already // precompiled from .gno to .go. precompiled map[importPath]struct{} } -func newPrecompileOptions(flags precompileFlags) *precompileOptions { - return &precompileOptions{flags, map[importPath]struct{}{}} +func newPrecompileOptions(cfg *precompileCfg) *precompileOptions { + return &precompileOptions{cfg, map[importPath]struct{}{}} } -func (p *precompileOptions) getFlags() precompileFlags { - return p.flags +func (p *precompileOptions) getFlags() *precompileCfg { + return p.cfg } func (p *precompileOptions) isPrecompiled(pkg importPath) bool { @@ -55,11 +47,69 @@ func (p *precompileOptions) markAsPrecompiled(pkg importPath) { p.precompiled[pkg] = struct{}{} } -func precompileApp(cmd *command.Command, args []string, f interface{}) error { - flags := f.(precompileFlags) +func newPrecompileCmd(io *commands.IO) *commands.Command { + cfg := &precompileCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "precompile", + ShortUsage: "precompile [flags] [...]", + ShortHelp: "Precompiles .gno files to .go", + }, + cfg, + func(_ context.Context, args []string) error { + return execPrecompile(cfg, args, io) + }, + ) +} + +func (c *precompileCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.verbose, + "verbose", + false, + "verbose output when running", + ) + + fs.BoolVar( + &c.skipFmt, + "skip-fmt", + false, + "do not check syntax of generated .go files", + ) + + fs.BoolVar( + &c.skipImports, + "skip-imports", + false, + "do not precompile imports recursively", + ) + + fs.StringVar( + &c.goBinary, + "go-binary", + "go", + "go binary to use for building", + ) + + fs.StringVar( + &c.gofmtBinary, + "go-fmt-binary", + "gofmt", + "gofmt binary to use for syntax checking", + ) + + fs.StringVar( + &c.output, + "output", + ".", + "output directory", + ) +} + +func execPrecompile(cfg *precompileCfg, args []string, io *commands.IO) error { if len(args) < 1 { - cmd.ErrPrintfln("Usage: precompile [precompile flags] [packages]") - return errors.New("invalid args") + return flag.ErrHelp } // precompile .gno files. @@ -68,13 +118,14 @@ func precompileApp(cmd *command.Command, args []string, f interface{}) error { return fmt.Errorf("list paths: %w", err) } - opts := newPrecompileOptions(flags) + opts := newPrecompileOptions(cfg) errCount := 0 for _, filepath := range paths { err = precompileFile(filepath, opts) if err != nil { err = fmt.Errorf("%s: precompile: %w", filepath, err) - cmd.ErrPrintfln("%s", err.Error()) + io.ErrPrintfln("%s", err.Error()) + errCount++ } } @@ -108,12 +159,12 @@ func precompilePkg(pkgPath importPath, opts *precompileOptions) error { func precompileFile(srcPath string, opts *precompileOptions) error { flags := opts.getFlags() - gofmt := flags.GofmtBinary + gofmt := flags.gofmtBinary if gofmt == "" { - gofmt = defaultPrecompileFlags.GofmtBinary + gofmt = "gofmt" } - if flags.Verbose { + if flags.verbose { fmt.Fprintf(os.Stderr, "%s\n", srcPath) } @@ -134,8 +185,8 @@ func precompileFile(srcPath string, opts *precompileOptions) error { // resolve target path var targetPath string - if flags.Output != defaultPrecompileFlags.Output { - path, err := ResolvePath(flags.Output, importPath(filepath.Dir(srcPath))) + if flags.output != "." { + path, err := ResolvePath(flags.output, importPath(filepath.Dir(srcPath))) if err != nil { return fmt.Errorf("resolve output path: %w", err) } @@ -151,7 +202,7 @@ func precompileFile(srcPath string, opts *precompileOptions) error { } // check .go fmt, if `SkipFmt` sets to false. - if !flags.SkipFmt { + if !flags.skipFmt { err = gno.PrecompileVerifyFile(targetPath, gofmt) if err != nil { return fmt.Errorf("check .go file: %w", err) @@ -159,7 +210,7 @@ func precompileFile(srcPath string, opts *precompileOptions) error { } // precompile imported packages, if `SkipImports` sets to false - if !flags.SkipImports { + if !flags.skipImports { importPaths := getPathsFromImportSpec(precompileRes.Imports) for _, path := range importPaths { precompilePkg(path, opts) diff --git a/cmd/gnodev/precompile_test.go b/cmd/gnodev/precompile_test.go index d8d3d5efefd..56f63b0de35 100644 --- a/cmd/gnodev/precompile_test.go +++ b/cmd/gnodev/precompile_test.go @@ -5,12 +5,8 @@ import "testing" func TestPrecompileApp(t *testing.T) { tc := []testMainCase{ { - args: []string{"precompile"}, - errShouldBe: "invalid args", - stderrShouldBe: "Usage: precompile [precompile flags] [packages]\n", - }, { - args: []string{"precompile", "--help"}, - stdoutShouldContain: "# precompileFlags options\n-", + args: []string{"precompile"}, + errShouldBe: "flag: help requested", }, // {args: []string{"precompile", "..."}, stdoutShouldContain: "..."}, diff --git a/cmd/gnodev/repl.go b/cmd/gnodev/repl.go index 30e09ac7f68..1a09aac3eef 100644 --- a/cmd/gnodev/repl.go +++ b/cmd/gnodev/repl.go @@ -1,48 +1,67 @@ package main import ( + "context" goerrors "errors" + "flag" "fmt" "io" "os" + "github.com/gnolang/gno/pkgs/commands" "golang.org/x/term" - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/errors" gno "github.com/gnolang/gno/pkgs/gnolang" "github.com/gnolang/gno/tests" ) -type replOptions struct { - Verbose bool `flag:"verbose" help:"verbose"` - RootDir string `flag:"root-dir" help:"clone location of github.com/gnolang/gno (gnodev tries to guess it)"` - // Run string `flag:"run" help:"test name filtering pattern"` - // Timeout time.Duration `flag:"timeout" help:"max execution time"` - // VM Options - // A flag about if we should download the production realms - // UseNativeLibs bool // experimental, but could be useful for advanced developer needs - // AutoImport bool - // ImportPkgs... +type replCfg struct { + verbose bool + rootDir string } -var defaultReplOptions = replOptions{ - Verbose: false, - RootDir: "", +func newReplCmd() *commands.Command { + cfg := &replCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "repl", + ShortUsage: "repl [flags]", + ShortHelp: "Starts a GnoVM REPL", + }, + cfg, + func(_ context.Context, args []string) error { + return execRepl(cfg, args) + }, + ) +} + +func (c *replCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.verbose, + "verbose", + false, + "verbose output when running", + ) + + fs.StringVar( + &c.rootDir, + "root-dir", + "", + "clone location of github.com/gnolang/gno (gnodev tries to guess it)", + ) } -func replApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(replOptions) +func execRepl(cfg *replCfg, args []string) error { if len(args) > 0 { - cmd.ErrPrintfln("Usage: repl [flags]") - return errors.New("invalid args") + return flag.ErrHelp } - if opts.RootDir == "" { - opts.RootDir = guessRootDir() + if cfg.rootDir == "" { + cfg.rootDir = guessRootDir() } - return runRepl(opts.RootDir, opts.Verbose) + return runRepl(cfg.rootDir, cfg.verbose) } func runRepl(rootDir string, verbose bool) error { diff --git a/cmd/gnodev/repl_test.go b/cmd/gnodev/repl_test.go index 10f7073e6c2..1786b498bdf 100644 --- a/cmd/gnodev/repl_test.go +++ b/cmd/gnodev/repl_test.go @@ -4,11 +4,7 @@ import "testing" func TestReplApp(t *testing.T) { tc := []testMainCase{ - // {args: []string{"repl"}, errShouldBe: "invalid args", stderrShouldBe: "Usage: repl [precompile flags] [packages]\n"}, - { - args: []string{"repl", "--help"}, - stdoutShouldContain: "# replOptions options\n-", - }, + {args: []string{"repl", "invalid-arg"}, errShouldBe: "flag: help requested"}, // args // {args: []string{"repl", "..."}, stdoutShouldContain: "..."}, diff --git a/cmd/gnodev/run.go b/cmd/gnodev/run.go index a075c7d471e..8155dfd6305 100644 --- a/cmd/gnodev/run.go +++ b/cmd/gnodev/run.go @@ -1,53 +1,69 @@ package main import ( - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/errors" + "context" + "flag" + + "github.com/gnolang/gno/pkgs/commands" gno "github.com/gnolang/gno/pkgs/gnolang" "github.com/gnolang/gno/tests" ) -type runOptions struct { - Verbose bool `flag:"verbose" help:"verbose"` - RootDir string `flag:"root-dir" help:"clone location of github.com/gnolang/gno (gnodev tries to guess it)"` - // Timeout time.Duration `flag:"timeout" help:"max execution time"` - // VM Options - // UseNativeLibs bool // experimental, but could be useful for advanced developer needs +type runCfg struct { + verbose bool + rootDir string } -var defaultRunOptions = runOptions{ - Verbose: false, - RootDir: "", +func newRunCmd(io *commands.IO) *commands.Command { + cfg := &runCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "run", + ShortUsage: "run [flags] [...]", + ShortHelp: "Runs the specified gno files", + }, + cfg, + func(_ context.Context, args []string) error { + return execRun(cfg, args, io) + }, + ) } -func runApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(runOptions) +func (c *runCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.verbose, + "verbose", + false, + "verbose output when running", + ) - if len(args) == 0 { - cmd.ErrPrintfln("Usage: run [flags] file.gno [file2.gno...]") + fs.StringVar( + &c.rootDir, + "root-dir", + "", + "clone location of github.com/gnolang/gno (gnodev tries to guess it)", + ) +} - return errors.New("invalid args") +func execRun(cfg *runCfg, args []string, io *commands.IO) error { + if len(args) == 0 { + return flag.ErrHelp } - if opts.RootDir == "" { - opts.RootDir = guessRootDir() + if cfg.rootDir == "" { + cfg.rootDir = guessRootDir() } - fnames := args - - return runRun(cmd, opts.RootDir, opts.Verbose, fnames) -} - -func runRun(cmd *command.Command, rootDir string, verbose bool, fnames []string) error { - stdin := cmd.In - stdout := cmd.Out - stderr := cmd.Err + stdin := io.In + stdout := io.Out + stderr := io.Err // init store and machine - testStore := tests.TestStore(rootDir, + testStore := tests.TestStore(cfg.rootDir, "", stdin, stdout, stderr, tests.ImportModeStdlibsPreferred) - if verbose { + if cfg.verbose { testStore.SetLogStoreOps(true) } @@ -58,8 +74,8 @@ func runRun(cmd *command.Command, rootDir string, verbose bool, fnames []string) }) // read files - files := make([]*gno.FileNode, len(fnames)) - for i, fname := range fnames { + files := make([]*gno.FileNode, len(args)) + for i, fname := range args { files[i] = gno.MustReadFile(fname) } diff --git a/cmd/gnodev/run_test.go b/cmd/gnodev/run_test.go index d46437f0bd7..bccd7d1190c 100644 --- a/cmd/gnodev/run_test.go +++ b/cmd/gnodev/run_test.go @@ -5,22 +5,22 @@ import "testing" func TestRunApp(t *testing.T) { tc := []testMainCase{ { - args: []string{"run"}, - errShouldBe: "invalid args", - stderrShouldBe: "Usage: run [flags] file.gno [file2.gno...]\n", - }, { - args: []string{"run", "--help"}, - stdoutShouldContain: "# runOptions options\n-", - }, { + args: []string{"run"}, + errShouldBe: "flag: help requested", + }, + { args: []string{"run", "../../tests/integ/run-main/main.gno"}, stdoutShouldContain: "hello world!", - }, { + }, + { args: []string{"run", "../../tests/integ/run-main/"}, recoverShouldContain: "read ../../tests/integ/run-main/: is a directory", // FIXME: should work - }, { + }, + { args: []string{"run", "../../tests/integ/does-not-exist"}, recoverShouldContain: "open ../../tests/integ/does-not-exist: no such file or directory", - }, { + }, + { args: []string{"run", "../../tests/integ/run-namedpkg/main.gno"}, recoverShouldContain: "expected package name [main] but got [namedpkg]", // FIXME: should work }, diff --git a/cmd/gnodev/test.go b/cmd/gnodev/test.go index 53225cec620..d2e66710ea3 100644 --- a/cmd/gnodev/test.go +++ b/cmd/gnodev/test.go @@ -2,9 +2,10 @@ package main import ( "bytes" + "context" "encoding/json" + "flag" "fmt" - "io" "log" "os" "path/filepath" @@ -13,7 +14,7 @@ import ( "text/template" "time" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/errors" gno "github.com/gnolang/gno/pkgs/gnolang" "github.com/gnolang/gno/pkgs/std" @@ -22,35 +23,81 @@ import ( "go.uber.org/multierr" ) -type testOptions struct { - Verbose bool `flag:"verbose" help:"verbose"` - RootDir string `flag:"root-dir" help:"clone location of github.com/gnolang/gno (gnodev tries to guess it)"` - Run string `flag:"run" help:"test name filtering pattern"` - Timeout time.Duration `flag:"timeout" help:"max execution time"` - Precompile bool `flag:"precompile" help:"precompiling gno to go before testing"` // TODO: precompile should be the default, but it needs to automatically precompile dependencies in memory. - UpdateGoldenTests bool `flag:"update-golden-tests" help:"writes actual as wanted in test comments"` - // VM Options - // A flag about if we should download the production realms - // UseNativeLibs bool // experimental, but could be useful for advanced developer needs +type testCfg struct { + verbose bool + rootDir string + run string + timeout time.Duration + precompile bool // TODO: precompile should be the default, but it needs to automatically precompile dependencies in memory. + updateGoldenTests bool } -var defaultTestOptions = testOptions{ - Verbose: false, - Run: "", - RootDir: "", - Timeout: 0, - Precompile: false, - UpdateGoldenTests: false, +func newTestCmd(io *commands.IO) *commands.Command { + cfg := &testCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "test", + ShortUsage: "test [flags] [...]", + ShortHelp: "Runs the tests for the specified packages", + }, + cfg, + func(_ context.Context, args []string) error { + return execTest(cfg, args, io) + }, + ) +} + +func (c *testCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.verbose, + "verbose", + false, + "verbose output when running", + ) + + fs.BoolVar( + &c.precompile, + "precompile", + false, + "precompile gno to go before testing", + ) + + fs.BoolVar( + &c.updateGoldenTests, + "update-golden-tests", + false, + "writes actual as wanted in test comments", + ) + + fs.StringVar( + &c.rootDir, + "root-dir", + "", + "clone location of github.com/gnolang/gno (gnodev tries to guess it)", + ) + + fs.StringVar( + &c.run, + "run", + "", + "test name filtering pattern", + ) + + fs.DurationVar( + &c.timeout, + "timeout", + 0, + "max execution time", + ) } -func testApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(testOptions) +func execTest(cfg *testCfg, args []string, io *commands.IO) error { if len(args) < 1 { - cmd.ErrPrintfln("Usage: test [test flags] [packages]") - return errors.New("invalid args") + return flag.ErrHelp } - verbose := opts.Verbose + verbose := cfg.verbose tempdirRoot, err := os.MkdirTemp("", "gno-precompile") if err != nil { @@ -66,8 +113,8 @@ func testApp(cmd *command.Command, args []string, iopts interface{}) error { } // guess opts.RootDir - if opts.RootDir == "" { - opts.RootDir = guessRootDir() + if cfg.rootDir == "" { + cfg.rootDir = guessRootDir() } pkgPaths, err := gnoPackagesFromArgs(args) @@ -75,46 +122,48 @@ func testApp(cmd *command.Command, args []string, iopts interface{}) error { return fmt.Errorf("list packages from args: %w", err) } - if opts.Timeout > 0 { + if cfg.timeout > 0 { go func() { - time.Sleep(opts.Timeout) - panic("test timed out after " + opts.Timeout.String()) + time.Sleep(cfg.timeout) + panic("test timed out after " + cfg.timeout.String()) }() } buildErrCount := 0 testErrCount := 0 for _, pkgPath := range pkgPaths { - if opts.Precompile { + if cfg.precompile { if verbose { - cmd.ErrPrintfln("=== PREC %s", pkgPath) + io.ErrPrintfln("=== PREC %s", pkgPath) } - precompileOpts := newPrecompileOptions(precompileFlags{ - Output: tempdirRoot, + precompileOpts := newPrecompileOptions(&precompileCfg{ + output: tempdirRoot, }) err := precompilePkg(importPath(pkgPath), precompileOpts) if err != nil { - cmd.ErrPrintln(err) - cmd.ErrPrintln("FAIL") - cmd.ErrPrintfln("FAIL %s", pkgPath) - cmd.ErrPrintln("FAIL") + io.ErrPrintln(err) + io.ErrPrintln("FAIL") + io.ErrPrintfln("FAIL %s", pkgPath) + io.ErrPrintln("FAIL") + buildErrCount++ continue } if verbose { - cmd.ErrPrintfln("=== BUILD %s", pkgPath) + io.ErrPrintfln("=== BUILD %s", pkgPath) } tempDir, err := ResolvePath(tempdirRoot, importPath(pkgPath)) if err != nil { - errors.New("cannot resolve build dir") + return errors.New("cannot resolve build dir") } err = goBuildFileOrPkg(tempDir, defaultBuildOptions) if err != nil { - cmd.ErrPrintln(err) - cmd.ErrPrintln("FAIL") - cmd.ErrPrintfln("FAIL %s", pkgPath) - cmd.ErrPrintln("FAIL") + io.ErrPrintln(err) + io.ErrPrintln("FAIL") + io.ErrPrintfln("FAIL %s", pkgPath) + io.ErrPrintln("FAIL") + buildErrCount++ continue } @@ -129,7 +178,7 @@ func testApp(cmd *command.Command, args []string, iopts interface{}) error { log.Fatal(err) } if len(unittestFiles) == 0 && len(filetestFiles) == 0 { - cmd.ErrPrintfln("? %s \t[no test files]", pkgPath) + io.ErrPrintfln("? %s \t[no test files]", pkgPath) continue } @@ -137,22 +186,22 @@ func testApp(cmd *command.Command, args []string, iopts interface{}) error { sort.Strings(filetestFiles) startedAt := time.Now() - err = gnoTestPkg(cmd, pkgPath, unittestFiles, filetestFiles, opts) + err = gnoTestPkg(pkgPath, unittestFiles, filetestFiles, cfg, io) duration := time.Since(startedAt) dstr := fmtDuration(duration) if err != nil { - cmd.ErrPrintfln("%s: test pkg: %v", pkgPath, err) - cmd.ErrPrintfln("FAIL") - cmd.ErrPrintfln("FAIL %s \t%s", pkgPath, dstr) - cmd.ErrPrintfln("FAIL") + io.ErrPrintfln("%s: test pkg: %v", pkgPath, err) + io.ErrPrintfln("FAIL") + io.ErrPrintfln("FAIL %s \t%s", pkgPath, dstr) + io.ErrPrintfln("FAIL") testErrCount++ } else { - cmd.ErrPrintfln("ok %s \t%s", pkgPath, dstr) + io.ErrPrintfln("ok %s \t%s", pkgPath, dstr) } } if testErrCount > 0 || buildErrCount > 0 { - cmd.ErrPrintfln("FAIL") + io.ErrPrintfln("FAIL") return fmt.Errorf("FAIL: %d build errors, %d test errors", buildErrCount, testErrCount) } @@ -160,20 +209,20 @@ func testApp(cmd *command.Command, args []string, iopts interface{}) error { } func gnoTestPkg( - cmd *command.Command, pkgPath string, unittestFiles, filetestFiles []string, - opts testOptions, + cfg *testCfg, + io *commands.IO, ) error { - verbose := opts.Verbose - rootDir := opts.RootDir - runFlag := opts.Run + verbose := cfg.verbose + rootDir := cfg.rootDir + runFlag := cfg.run filter := splitRegexp(runFlag) - stdin := cmd.In - stdout := cmd.Out - stderr := cmd.Err + stdin := io.In + stdout := io.Out + stderr := io.Err var errs error @@ -188,7 +237,7 @@ func gnoTestPkg( if !verbose { // TODO: speedup by ignoring if filter is file/*? - stdout = nopWriteCloser{} + stdout = commands.WriteNopCloser(nil) } // testing with *_test.gno @@ -202,7 +251,7 @@ func gnoTestPkg( { m := tests.TestMachine(testStore, stdout, "main") m.RunMemPackage(memPkg, true) - err := runTestFiles(cmd, testStore, m, tfiles, memPkg.Name, verbose, runFlag) + err := runTestFiles(m, tfiles, memPkg.Name, verbose, runFlag, io) if err != nil { errs = multierr.Append(errs, err) } @@ -214,7 +263,7 @@ func gnoTestPkg( if testPkgName != "" { m := tests.TestMachine(testStore, stdout, testPkgName) m.RunMemPackage(memPkg, true) - err := runTestFiles(cmd, testStore, m, ifiles, testPkgName, verbose, runFlag) + err := runTestFiles(m, ifiles, testPkgName, verbose, runFlag, io) if err != nil { errs = multierr.Append(errs, err) } @@ -233,7 +282,7 @@ func gnoTestPkg( startedAt := time.Now() if verbose { - cmd.ErrPrintfln("=== RUN %s", testName) + io.ErrPrintfln("=== RUN %s", testName) } var closer func() (string, error) @@ -242,25 +291,25 @@ func gnoTestPkg( } testFilePath := filepath.Join(pkgPath, testFileName) - err := tests.RunFileTest(rootDir, testFilePath, tests.WithSyncWanted(opts.UpdateGoldenTests)) + err := tests.RunFileTest(rootDir, testFilePath, tests.WithSyncWanted(cfg.updateGoldenTests)) duration := time.Since(startedAt) dstr := fmtDuration(duration) if err != nil { errs = multierr.Append(errs, err) - cmd.ErrPrintfln("--- FAIL: %s (%s)", testName, dstr) + io.ErrPrintfln("--- FAIL: %s (%s)", testName, dstr) if verbose { stdouterr, err := closer() if err != nil { panic(err) } - fmt.Fprintln(stderr, stdouterr) + fmt.Fprintln(os.Stderr, stdouterr) } continue } if verbose { - cmd.ErrPrintfln("--- PASS: %s (%s)", testName, dstr) + io.ErrPrintfln("--- PASS: %s (%s)", testName, dstr) } } } @@ -268,18 +317,13 @@ func gnoTestPkg( return errs } -type nopWriteCloser struct{ io.Writer } - -func (nopWriteCloser) Close() error { return nil } - func runTestFiles( - cmd *command.Command, - testStore gno.Store, m *gno.Machine, files *gno.FileSet, pkgName string, verbose bool, runFlag string, + io *commands.IO, ) error { var errs error @@ -301,7 +345,7 @@ func runTestFiles( for _, test := range testFuncs.Tests { if verbose { - cmd.ErrPrintfln("=== RUN %s", test.Name) + io.ErrPrintfln("=== RUN %s", test.Name) } testFuncStr := fmt.Sprintf("%q", test.Name) @@ -315,7 +359,7 @@ func runTestFiles( if ret == "" { err := errors.New("failed to execute unit test: %q", test.Name) errs = multierr.Append(errs, err) - cmd.ErrPrintfln("--- FAIL: %s (%v)", test.Name, duration) + io.ErrPrintfln("--- FAIL: %s (%v)", test.Name, duration) continue } @@ -324,30 +368,30 @@ func runTestFiles( err = json.Unmarshal([]byte(ret), &rep) if err != nil { errs = multierr.Append(errs, err) - cmd.ErrPrintfln("--- FAIL: %s (%s)", test.Name, dstr) + io.ErrPrintfln("--- FAIL: %s (%s)", test.Name, dstr) continue } switch { case rep.Filtered: - cmd.ErrPrintfln("--- FILT: %s", test.Name) + io.ErrPrintfln("--- FILT: %s", test.Name) // noop case rep.Skipped: if verbose { - cmd.ErrPrintfln("--- SKIP: %s", test.Name) + io.ErrPrintfln("--- SKIP: %s", test.Name) } case rep.Failed: err := errors.New("failed: %q", test.Name) errs = multierr.Append(errs, err) - cmd.ErrPrintfln("--- FAIL: %s (%s)", test.Name, dstr) + io.ErrPrintfln("--- FAIL: %s (%s)", test.Name, dstr) default: if verbose { - cmd.ErrPrintfln("--- PASS: %s (%s)", test.Name, dstr) + io.ErrPrintfln("--- PASS: %s (%s)", test.Name, dstr) } } if rep.Output != "" && (verbose || rep.Failed) { - cmd.ErrPrintfln("output: %s", rep.Output) + io.ErrPrintfln("output: %s", rep.Output) } } diff --git a/cmd/gnodev/test_test.go b/cmd/gnodev/test_test.go index ba394fa4895..a5cad7a321f 100644 --- a/cmd/gnodev/test_test.go +++ b/cmd/gnodev/test_test.go @@ -5,13 +5,10 @@ import "testing" func TestTest(t *testing.T) { tc := []testMainCase{ { - args: []string{"test"}, - errShouldBe: "invalid args", - stderrShouldBe: "Usage: test [test flags] [packages]\n", - }, { - args: []string{"test", "--help"}, - stdoutShouldContain: "# testOptions options\n-", - }, { + args: []string{"test"}, + errShouldBe: "flag: help requested", + }, + { args: []string{"test", "../../examples/gno.land/p/demo/rand"}, stderrShouldContain: "ok ./../../examples/gno.land/p/demo/rand \t", }, { @@ -30,19 +27,19 @@ func TestTest(t *testing.T) { args: []string{"test", "../../tests/integ/minimalist-gno3"}, stderrShouldContain: "ok ", }, { - args: []string{"test", "../../tests/integ/valid1", "--verbose"}, + args: []string{"test", "--verbose", "../../tests/integ/valid1"}, stderrShouldContain: "ok ", }, { args: []string{"test", "../../tests/integ/valid2"}, stderrShouldContain: "ok ", }, { - args: []string{"test", "../../tests/integ/valid2", "--verbose"}, + args: []string{"test", "--verbose", "../../tests/integ/valid2"}, stderrShouldContain: "ok ", }, { args: []string{"test", "../../tests/integ/empty-gno1"}, stderrShouldBe: "? ./../../tests/integ/empty-gno1 \t[no test files]\n", }, { - args: []string{"test", "../../tests/integ/empty-gno1", "--precompile"}, + args: []string{"test", "--precompile", "../../tests/integ/empty-gno1"}, errShouldBe: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno1/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'", }, { @@ -50,7 +47,7 @@ func TestTest(t *testing.T) { recoverShouldBe: "empty.gno:1:1: expected 'package', found 'EOF'", }, { // FIXME: better error handling + rename dontcare.gno with actual test file - args: []string{"test", "../../tests/integ/empty-gno2", "--precompile"}, + args: []string{"test", "--precompile", "../../tests/integ/empty-gno2"}, errShouldContain: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno2/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'", }, { @@ -58,23 +55,23 @@ func TestTest(t *testing.T) { recoverShouldBe: "../../tests/integ/empty-gno3/empty_filetest.gno:1:1: expected 'package', found 'EOF'", }, { // FIXME: better error handling - args: []string{"test", "../../tests/integ/empty-gno3", "--precompile"}, + args: []string{"test", "--precompile", "../../tests/integ/empty-gno3"}, errShouldContain: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno3/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'", }, { - args: []string{"test", "../../tests/integ/failing1", "--verbose"}, + args: []string{"test", "--verbose", "../../tests/integ/failing1"}, errShouldBe: "FAIL: 0 build errors, 1 test errors", stderrShouldContain: "FAIL: TestAlwaysFailing", }, { - args: []string{"test", "../../tests/integ/failing1", "--verbose", "--precompile"}, + args: []string{"test", "--verbose", "--precompile", "../../tests/integ/failing1"}, errShouldBe: "FAIL: 0 build errors, 1 test errors", stderrShouldContain: "FAIL: TestAlwaysFailing", }, { - args: []string{"test", "../../tests/integ/failing2", "--verbose"}, + args: []string{"test", "--verbose", "../../tests/integ/failing2"}, recoverShouldBe: "fail on ../../tests/integ/failing2/failing_filetest.gno: got unexpected error: beep boop", stderrShouldContain: "== RUN file/failing_filetest.gno", }, { - args: []string{"test", "../../tests/integ/failing2", "--verbose", "--precompile"}, + args: []string{"test", "--verbose", "--precompile", "../../tests/integ/failing2"}, stderrShouldBe: "=== PREC ./../../tests/integ/failing2\n=== BUILD ./../../tests/integ/failing2\n=== RUN file/failing_filetest.gno\n", recoverShouldBe: "fail on ../../tests/integ/failing2/failing_filetest.gno: got unexpected error: beep boop", }, { @@ -82,60 +79,60 @@ func TestTest(t *testing.T) { stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose"}, + args: []string{"test", "--verbose", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", ".*"}, + args: []string{"test", "--verbose", "--run", ".*", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", "NoExists"}, + args: []string{"test", "--verbose", "--run", "NoExists", "../../examples/gno.land/p/demo/ufmt"}, stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", ".*/hello"}, + args: []string{"test", "--verbose", "--run", ".*/hello", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", ".*/hi"}, + args: []string{"test", "--verbose", "--run", ".*/hi", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", ".*/NoExists"}, + args: []string{"test", "--verbose", "--run", ".*/NoExists", "../../examples/gno.land/p/demo/ufmt"}, stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", ".*/hello/NoExists"}, + args: []string{"test", "--verbose", "--run", ".*/hello/NoExists", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", "Sprintf/"}, + args: []string{"test", "--verbose", "--run", "Sprintf/", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", "Sprintf/.*"}, + args: []string{"test", "--verbose", "--run", "Sprintf/.*", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--run", "Sprintf/hello"}, + args: []string{"test", "--verbose", "--run", "Sprintf/hello", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, { - args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--timeout", "100000000000" /* 100s */}, + args: []string{"test", "--verbose", "--timeout", "1000000s", "../../examples/gno.land/p/demo/ufmt"}, stdoutShouldContain: "RUN TestSprintf", stderrShouldContain: "ok ./../../examples/gno.land/p/demo/ufmt", }, // TODO: when 'gnodev test' will by default imply running precompile, we should use the following tests. - //{args: []string{"test", "../../tests/integ/empty-gno1", "--no-precompile"}, stderrShouldBe: "? ./../../tests/integ/empty-gno1 \t[no test files]\n"}, - //{args: []string{"test", "../../tests/integ/empty-gno1"}, errShouldBe: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno1/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'"}, - //{args: []string{"test", "../../tests/integ/empty-gno2", "--no-precompile"}, recoverShouldBe: "empty.gno:1:1: expected 'package', found 'EOF'"}, // FIXME: better error handling + rename dontcare.gno with actual test file - //{args: []string{"test", "../../tests/integ/empty-gno2"}, errShouldContain: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno2/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'"}, - //{args: []string{"test", "../../tests/integ/empty-gno3", "--no-precompile"}, recoverShouldBe: "../../tests/integ/empty-gno3/empty_filetest.gno:1:1: expected 'package', found 'EOF'"}, // FIXME: better error handling - //{args: []string{"test", "../../tests/integ/empty-gno3"}, errShouldContain: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno3/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'"}, - //{args: []string{"test", "../../tests/integ/failing1", "--verbose", "--no-precompile"}, errShouldBe: "FAIL: 0 build errors, 1 test errors", stderrShouldContain: "FAIL: TestAlwaysFailing"}, - //{args: []string{"test", "../../tests/integ/failing1", "--verbose"}, errShouldBe: "FAIL: 0 build errors, 1 test errors", stderrShouldContain: "FAIL: TestAlwaysFailing"}, - //{args: []string{"test", "../../tests/integ/failing2", "--verbose", "--no-precompile"}, recoverShouldBe: "fail on ../../tests/integ/failing2/failing_filetest.gno: got unexpected error: beep boop", stderrShouldContain: "== RUN file/failing_filetest.gno"}, - //{args: []string{"test", "../../tests/integ/failing2", "--verbose"}, stderrShouldBe: "=== PREC ./../../tests/integ/failing2\n=== BUILD ./../../tests/integ/failing2\n=== RUN file/failing_filetest.gno\n", recoverShouldBe: "fail on ../../tests/integ/failing2/failing_filetest.gno: got unexpected error: beep boop"}, + // {args: []string{"test", "../../tests/integ/empty-gno1", "--no-precompile"}, stderrShouldBe: "? ./../../tests/integ/empty-gno1 \t[no test files]\n"}, + // {args: []string{"test", "../../tests/integ/empty-gno1"}, errShouldBe: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno1/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'"}, + // {args: []string{"test", "../../tests/integ/empty-gno2", "--no-precompile"}, recoverShouldBe: "empty.gno:1:1: expected 'package', found 'EOF'"}, // FIXME: better error handling + rename dontcare.gno with actual test file + // {args: []string{"test", "../../tests/integ/empty-gno2"}, errShouldContain: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno2/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'"}, + // {args: []string{"test", "../../tests/integ/empty-gno3", "--no-precompile"}, recoverShouldBe: "../../tests/integ/empty-gno3/empty_filetest.gno:1:1: expected 'package', found 'EOF'"}, // FIXME: better error handling + // {args: []string{"test", "../../tests/integ/empty-gno3"}, errShouldContain: "FAIL: 1 build errors, 0 test errors", stderrShouldContain: "../../tests/integ/empty-gno3/empty.gno: parse: tmp.gno:1:1: expected 'package', found 'EOF'"}, + // {args: []string{"test", "../../tests/integ/failing1", "--verbose", "--no-precompile"}, errShouldBe: "FAIL: 0 build errors, 1 test errors", stderrShouldContain: "FAIL: TestAlwaysFailing"}, + // {args: []string{"test", "../../tests/integ/failing1", "--verbose"}, errShouldBe: "FAIL: 0 build errors, 1 test errors", stderrShouldContain: "FAIL: TestAlwaysFailing"}, + // {args: []string{"test", "../../tests/integ/failing2", "--verbose", "--no-precompile"}, recoverShouldBe: "fail on ../../tests/integ/failing2/failing_filetest.gno: got unexpected error: beep boop", stderrShouldContain: "== RUN file/failing_filetest.gno"}, + // {args: []string{"test", "../../tests/integ/failing2", "--verbose"}, stderrShouldBe: "=== PREC ./../../tests/integ/failing2\n=== BUILD ./../../tests/integ/failing2\n=== RUN file/failing_filetest.gno\n", recoverShouldBe: "fail on ../../tests/integ/failing2/failing_filetest.gno: got unexpected error: beep boop"}, // {args: []string{"test", "../../examples/gno.land/p/demo/ufmt", "--verbose", "--timeout", "10000" /* 10µs */}, recoverShouldContain: "test timed out after"}, // FIXME: should be testable } testMainCaseRun(t, tc) diff --git a/cmd/gnofaucet/README.md b/cmd/gnofaucet/README.md index 7855c5c9705..ddee1db1bc4 100644 --- a/cmd/gnofaucet/README.md +++ b/cmd/gnofaucet/README.md @@ -1,10 +1,10 @@ -# Start a local faucet +# Start a local faucet ## Step1: Import test1 key If you have imported the test1 key skip to Step2 - ./build/gnokey add test1 --recover + ./build/gnokey add --recover test1 At prompt, input and confirm your password to protect the imported private key. @@ -20,19 +20,19 @@ Make sure you have started gnoland ## Step3: - ./build/gnofaucet serve test1 --chain-id dev + ./build/gnofaucet serve --chain-id dev test1 -By default, the faucet sends out 1,000,000ugnot (1gnot) per request. If this is your local faucet, you can be a bit generous to yourself with --send flag. With the following, the faucet will give you 500gnot per request. +By default, the faucet sends out 1,000,000ugnot (1gnot) per request. If this is your local faucet, you can be a bit +generous to yourself with --send flag. With the following, the faucet will give you 500gnot per request. - ./build/gnofaucet serve test1 --chain-id dev --send 5000000000ugnot - + ./build/gnofaucet serve --chain-id dev --send 5000000000ugnot test1 ## Step4: -Make sure you have started website - +Make sure you have started website + ./build/website - + Request testing tokens from following URL, Have fun! http://localhost:8888/faucet diff --git a/cmd/gnofaucet/main.go b/cmd/gnofaucet/main.go new file mode 100644 index 00000000000..b4709391652 --- /dev/null +++ b/cmd/gnofaucet/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/gnolang/gno/pkgs/commands" +) + +func main() { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + LongHelp: "Starts the fund faucet that can be used by users", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newServeCmd(), + ) + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v", err) + + os.Exit(1) + } +} diff --git a/cmd/gnofaucet/gnofaucet.go b/cmd/gnofaucet/serve.go similarity index 64% rename from cmd/gnofaucet/gnofaucet.go rename to cmd/gnofaucet/serve.go index 3e5319fb4d0..07f551fdff1 100644 --- a/cmd/gnofaucet/gnofaucet.go +++ b/cmd/gnofaucet/serve.go @@ -1,18 +1,19 @@ package main import ( + "context" "encoding/json" + "flag" "fmt" "net" "net/http" - "os" "strings" "time" "github.com/gnolang/gno/gnoland" "github.com/gnolang/gno/pkgs/amino" rpcclient "github.com/gnolang/gno/pkgs/bft/rpc/client" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto" "github.com/gnolang/gno/pkgs/crypto/keys" "github.com/gnolang/gno/pkgs/crypto/keys/client" @@ -33,95 +34,142 @@ type SiteVerifyResponse struct { ErrorCodes []string `json:"error-codes"` } -type ( - AppItem = command.AppItem - AppList = command.AppList -) - -var mainApps AppList = []AppItem{ - {serveApp, "serve", "serve faucet", DefaultServeOptions}, -} - -func runMain(cmd *command.Command, exec string, args []string) error { - // show help message. - if len(args) == 0 || args[0] == "help" || args[0] == "--help" { - cmd.Println("available subcommands:") - for _, appItem := range mainApps { - cmd.Printf(" %s - %s\n", appItem.Name, appItem.Desc) - } - return nil - } - - // switch on first argument. - for _, appItem := range mainApps { - if appItem.Name == args[0] { - err := cmd.Run(appItem.App, args[1:], appItem.Defaults) - return err // done - } - } - - // unknown app command! - return errors.New("unknown command " + args[0]) +type config struct { + client.BaseOptions // home, ... + + ChainID string + GasWanted int64 + GasFee string + Memo string + TestTo string + Send string + CaptchaSecret string + IsBehindProxy bool + InsecurePasswordStdin bool } -func main() { - cmd := command.NewStdCommand() - exec := os.Args[0] - args := os.Args[1:] - err := runMain(cmd, exec, args) - if err != nil { - cmd.ErrPrintfln("%s", err.Error()) - cmd.ErrPrintfln("%#v", err) - return // exit - } +func newServeCmd() *commands.Command { + cfg := &config{} + + return commands.NewCommand( + commands.Metadata{ + Name: "serve", + ShortUsage: "serve [flags] ", + LongHelp: "Serves the gno.land faucet to users", + }, + cfg, + func(_ context.Context, args []string) error { + return execServe(cfg, args, commands.NewDefaultIO()) + }, + ) } -//---------------------------------------- -// serveApp - -type serveOptions struct { - client.BaseOptions // home, ... - ChainID string `flag:"chain-id" help:"chain id"` - GasWanted int64 `flag:"gas-wanted" help:"gas requested for tx"` - GasFee string `flag:"gas-fee" help:"gas payment fee"` - Memo string `flag:"memo" help:"any descriptive text"` - TestTo string `flag:"test-to" help:"test addr (optional)"` - Send string `flag:"send" help:"send coins"` - CaptchaSecret string `flag:"captcha-secret" help:"recaptcha secret key (if empty, captcha are disabled)"` - IsBehindProxy bool `flag:"is-behind-proxy" help:"use X-Forwarded-For IP for throttling"` - InsecurePasswordStdin bool `flag:"insecure-password-stdin" help:"WARNING! take password from stdin"` +func (c *config) RegisterFlags(fs *flag.FlagSet) { + // Base config options + fs.StringVar( + &c.BaseOptions.Home, + "home", + client.DefaultBaseOptions.Home, + "home directory", + ) + + fs.StringVar( + &c.BaseOptions.Remote, + "remote", + client.DefaultBaseOptions.Remote, + "remote node URL", + ) + + fs.BoolVar( + &c.BaseOptions.Quiet, + "quiet", + client.DefaultBaseOptions.Quiet, + "for parsing output", + ) + + // Command options + fs.StringVar( + &c.ChainID, + "chain-id", + "", + "the ID of the chain", + ) + + fs.Int64Var( + &c.GasWanted, + "gas-wanted", + 50000, + "gas requested for the tx", + ) + + fs.StringVar( + &c.GasFee, + "gas-fee", + "1000000ugnot", + "gas payment fee", + ) + + fs.StringVar( + &c.Memo, + "memo", + "", + "any descriptive text", + ) + + fs.StringVar( + &c.TestTo, + "test-to", + "", + "test address (optional)", + ) + + fs.StringVar( + &c.Send, + "send", + "1000000ugnot", + "send coins", + ) + + fs.StringVar( + &c.CaptchaSecret, + "captcha-secret", + "", + "recaptcha secret key (if empty, captcha are disabled)", + ) + + fs.BoolVar( + &c.IsBehindProxy, + "is-behind-proxy", + false, + "use X-Forwarded-For IP for throttling", + ) + + fs.BoolVar( + &c.InsecurePasswordStdin, + "insecure-password-stdin", + false, + "WARNING! take password from stdin", + ) } -var DefaultServeOptions = serveOptions{ - BaseOptions: client.DefaultBaseOptions, - ChainID: "", // must override - GasWanted: 50000, - GasFee: "1000000ugnot", - Memo: "", - TestTo: "", - Send: "1000000ugnot", - CaptchaSecret: "", - IsBehindProxy: false, - InsecurePasswordStdin: false, -} - -func serveApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(serveOptions) +func execServe(cfg *config, args []string, io *commands.IO) error { if len(args) != 1 { - cmd.ErrPrintfln("Usage: serve ") - return errors.New("invalid args") + return flag.ErrHelp } - if opts.ChainID == "" { + + if cfg.ChainID == "" { return errors.New("chain-id not specified") } - if opts.GasWanted == 0 { + + if cfg.GasWanted == 0 { return errors.New("gas-wanted not specified") } - if opts.GasFee == "" { + + if cfg.GasFee == "" { return errors.New("gas-fee not specified") } - remote := opts.Remote + remote := cfg.Remote if remote == "" || remote == "y" { return errors.New("missing remote url") } @@ -130,7 +178,7 @@ func serveApp(cmd *command.Command, args []string, iopts interface{}) error { // XXX XXX // Read supply account pubkey. name := args[0] - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(cfg.Home) if err != nil { return err } @@ -139,15 +187,11 @@ func serveApp(cmd *command.Command, args []string, iopts interface{}) error { return err } fromAddr := info.GetAddress() - // pub := info.GetPubKey() // query for initial number and sequence. path := fmt.Sprintf("auth/accounts/%s", fromAddr.String()) data := []byte(nil) - opts2 := rpcclient.ABCIQueryOptions{ - // Height: height, XXX - // Prove: false, XXX - } + opts2 := rpcclient.ABCIQueryOptions{} qres, err := cli.ABCIQueryWithOptions( path, data, opts2) if err != nil { @@ -168,32 +212,34 @@ func serveApp(cmd *command.Command, args []string, iopts interface{}) error { // Test by signing a dummy message; const dummy = "test" var pass string - if opts.Quiet { - pass, err = cmd.GetPassword("", opts.InsecurePasswordStdin) + if cfg.Quiet { + pass, err = io.GetPassword("", cfg.InsecurePasswordStdin) } else { - pass, err = cmd.GetPassword("Enter password.", opts.InsecurePasswordStdin) + pass, err = io.GetPassword("Enter password", cfg.InsecurePasswordStdin) } + if err != nil { return err } + _, _, err = kb.Sign(name, pass, []byte(dummy)) if err != nil { return err } // Parse send amount. - send, err := std.ParseCoins(opts.Send) + send, err := std.ParseCoins(cfg.Send) if err != nil { return errors.Wrap(err, "parsing send coins") } // Parse test-to address. If present, send and quit. - if opts.TestTo != "" { - testToAddr, err := crypto.AddressFromBech32(opts.TestTo) + if cfg.TestTo != "" { + testToAddr, err := crypto.AddressFromBech32(cfg.TestTo) if err != nil { return err } - err = sendAmountTo(cmd, cli, name, pass, testToAddr, accountNumber, sequence, send, opts) + err = sendAmountTo(cfg, cli, io, name, pass, testToAddr, accountNumber, sequence, send) return err } @@ -204,7 +250,7 @@ func serveApp(cmd *command.Command, args []string, iopts interface{}) error { // handle route using handler function http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { host := "" - if !opts.IsBehindProxy { + if !cfg.IsBehindProxy { addr := r.RemoteAddr host_, _, err := net.SplitHostPort(addr) if err != nil { @@ -239,7 +285,7 @@ func serveApp(cmd *command.Command, args []string, iopts interface{}) error { // only when command line argument 'captcha-secret' has entered > captcha are enabled. // veryify captcha - if opts.CaptchaSecret != "" { + if cfg.CaptchaSecret != "" { passedMsg := r.Form["g-recaptcha-response"] if passedMsg == nil { fmt.Println(ip, "no 'captcha' request") @@ -249,7 +295,7 @@ func serveApp(cmd *command.Command, args []string, iopts interface{}) error { capMsg := strings.TrimSpace(passedMsg[0]) - if err := checkRecaptcha(opts.CaptchaSecret, capMsg); err != nil { + if err := checkRecaptcha(cfg.CaptchaSecret, capMsg); err != nil { fmt.Printf("%s recaptcha failed; %v\n", ip, err) w.Write([]byte("Unauthorized")) return @@ -272,7 +318,7 @@ func serveApp(cmd *command.Command, args []string, iopts interface{}) error { w.Write([]byte("invalid address format")) return } - err = sendAmountTo(cmd, cli, name, pass, toAddr, accountNumber, sequence, send, opts) + err = sendAmountTo(cfg, cli, io, name, pass, toAddr, accountNumber, sequence, send) if err != nil { fmt.Println(ip, "faucet failed", err) w.Write([]byte("faucet failed")) @@ -296,9 +342,19 @@ func serveApp(cmd *command.Command, args []string, iopts interface{}) error { return nil } -func sendAmountTo(cmd *command.Command, cli rpcclient.Client, name, pass string, toAddr crypto.Address, accountNumber, sequence uint64, send std.Coins, opts serveOptions) error { +func sendAmountTo( + cfg *config, + cli rpcclient.Client, + io *commands.IO, + name, + pass string, + toAddr crypto.Address, + accountNumber, + sequence uint64, + send std.Coins, +) error { // Read supply account pubkey. - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(cfg.Home) if err != nil { return err } @@ -310,8 +366,8 @@ func sendAmountTo(cmd *command.Command, cli rpcclient.Client, name, pass string, pub := info.GetPubKey() // parse gas wanted & fee. - gaswanted := opts.GasWanted - gasfee, err := std.ParseCoin(opts.GasFee) + gaswanted := cfg.GasWanted + gasfee, err := std.ParseCoin(cfg.GasFee) if err != nil { return errors.Wrap(err, "parsing gas fee coin") } @@ -326,7 +382,7 @@ func sendAmountTo(cmd *command.Command, cli rpcclient.Client, name, pass string, Msgs: []std.Msg{msg}, Fee: std.NewFee(gaswanted, gasfee), Signatures: nil, - Memo: opts.Memo, + Memo: cfg.Memo, } // fill tx signatures. signers := tx.GetSigners() @@ -345,7 +401,7 @@ func sendAmountTo(cmd *command.Command, cli rpcclient.Client, name, pass string, // fmt.Println("will sign:", string(amino.MustMarshalJSON(tx))) // get sign-bytes and make signature. - chainID := opts.ChainID + chainID := cfg.ChainID signbz := tx.GetSignBytes(chainID, accountNumber, sequence) sig, _, err := kb.Sign(name, pass, signbz) if err != nil { @@ -382,10 +438,10 @@ func sendAmountTo(cmd *command.Command, cli rpcclient.Client, name, pass string, } else if bres.DeliverTx.IsErr() { return errors.New("transaction failed %#v\nlog %s", bres, bres.DeliverTx.Log) } else { - cmd.Println(string(bres.DeliverTx.Data)) - cmd.Println("OK!") - cmd.Println("GAS WANTED:", bres.DeliverTx.GasWanted) - cmd.Println("GAS USED: ", bres.DeliverTx.GasUsed) + io.Println(string(bres.DeliverTx.Data)) + io.Println("OK!") + io.Println("GAS WANTED:", bres.DeliverTx.GasWanted) + io.Println("GAS USED: ", bres.DeliverTx.GasUsed) } return nil } diff --git a/cmd/gnokey/gnokeys.go b/cmd/gnokey/gnokeys.go deleted file mode 100644 index 1f08f567d42..00000000000 --- a/cmd/gnokey/gnokeys.go +++ /dev/null @@ -1,441 +0,0 @@ -// Dedicated to my love, Lexi. -package main - -import ( - "fmt" - "os" - - "github.com/gnolang/gno/pkgs/amino" - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/crypto" - "github.com/gnolang/gno/pkgs/crypto/keys" - "github.com/gnolang/gno/pkgs/crypto/keys/client" - "github.com/gnolang/gno/pkgs/errors" - gno "github.com/gnolang/gno/pkgs/gnolang" - "github.com/gnolang/gno/pkgs/sdk/bank" - "github.com/gnolang/gno/pkgs/sdk/vm" - "github.com/gnolang/gno/pkgs/std" -) - -func main() { - cmd := command.NewStdCommand() - exec := os.Args[0] - args := os.Args[1:] - // extend default crypto/keys/client with maketx. - client.AddApp(makeTxApp, "maketx", "compose a tx document to sign", nil) - err := client.RunMain(cmd, exec, args) - if err != nil { - cmd.ErrPrintfln("%s", err.Error()) - cmd.ErrPrintfln("%#v", err) - return // exit - } -} - -var makeTxApps client.AppList = []client.AppItem{ - { - makeAddPackageTxApp, - "addpkg", "upload new package", - defaultMakeAddPackageTxOptions, - }, - { - makeCallTxApp, - "call", "call public function", - defaultMakeCallTxOptions, - }, - { - makeSendTxApp, - "send", "send coins", - defaultMakeSendTxOptions, - }, -} - -func makeTxApp(cmd *command.Command, args []string, iopts interface{}) error { - // show help message. - if len(args) == 0 || args[0] == "help" || args[0] == "--help" { - cmd.Println("available subcommands:") - for _, appItem := range makeTxApps { - cmd.Printf(" %s - %s\n", appItem.Name, appItem.Desc) - } - return nil - } - - // switch on first argument. - for _, appItem := range makeTxApps { - if appItem.Name == args[0] { - err := cmd.Run(appItem.App, args[1:], appItem.Defaults) - return err // done - } - } - - // unknown app subcommand! - return errors.New("unknown subcommand " + args[0]) -} - -type SignBroadcastOptions struct { - GasWanted int64 `flag:"gas-wanted" help:"gas requested for tx"` - GasFee string `flag:"gas-fee" help:"gas payment fee"` - Memo string `flag:"memo" help:"any descriptive text"` - - Broadcast bool `flag:"broadcast" help:"sign and broadcast"` - ChainID string `flag:"chainid" help:"chainid to sign for (only useful if --broadcast)"` -} - -var defaultSignBroadcastOptions = SignBroadcastOptions{ - GasWanted: 0, - GasFee: "", - Memo: "", - Broadcast: false, - ChainID: "dev", -} - -//---------------------------------------- -// makeAddPackageTx - -type makeAddPackageTxOptions struct { - client.BaseOptions // home,... - SignBroadcastOptions // gas-wanted, gas-fee, memo, ... - PkgPath string `flag:"pkgpath" help:"package path (required)"` - PkgDir string `flag:"pkgdir" help:"path to package files (required)"` - Deposit string `flag:"deposit" help:"deposit coins"` -} - -var defaultMakeAddPackageTxOptions = makeAddPackageTxOptions{ - BaseOptions: client.DefaultBaseOptions, - SignBroadcastOptions: defaultSignBroadcastOptions, - PkgPath: "", // must override - PkgDir: "", // must override - Deposit: "", -} - -func makeAddPackageTxApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(makeAddPackageTxOptions) - if opts.PkgPath == "" { - return errors.New("pkgpath not specified") - } - if opts.PkgDir == "" { - return errors.New("pkgdir not specified") - } - if len(args) != 1 { - cmd.ErrPrintfln("Usage: addpkg ") - return errors.New("invalid args") - } - - // read account pubkey. - nameOrBech32 := args[0] - kb, err := keys.NewKeyBaseFromDir(opts.Home) - if err != nil { - return err - } - info, err := kb.GetByNameOrAddress(nameOrBech32) - if err != nil { - return err - } - creator := info.GetAddress() - // info.GetPubKey() - - // parse deposit. - deposit, err := std.ParseCoins(opts.Deposit) - if err != nil { - panic(err) - } - - // open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(opts.PkgDir, opts.PkgPath) - - // precompile and validate syntax - err = gno.PrecompileAndCheckMempkg(memPkg) - if err != nil { - panic(err) - } - - // parse gas wanted & fee. - gaswanted := opts.GasWanted - gasfee, err := std.ParseCoin(opts.GasFee) - if err != nil { - panic(err) - } - // construct msg & tx and marshal. - msg := vm.MsgAddPackage{ - Creator: creator, - Package: memPkg, - Deposit: deposit, - } - tx := std.Tx{ - Msgs: []std.Msg{msg}, - Fee: std.NewFee(gaswanted, gasfee), - Signatures: nil, - Memo: opts.Memo, - } - - if opts.Broadcast { - err := signAndBroadcast(cmd, args, tx, opts.BaseOptions, opts.SignBroadcastOptions) - if err != nil { - return err - } - } else { - fmt.Println(string(amino.MustMarshalJSON(tx))) - } - return nil -} - -//---------------------------------------- -// makeCallTxApp - -type makeCallTxOptions struct { - client.BaseOptions // home,... - SignBroadcastOptions // gas-wanted, gas-fee, memo, ... - Send string `flag:"send" help:"send coins"` - PkgPath string `flag:"pkgpath" help:"package path (required)"` - Func string `flag:"func" help:"contract to call (required)"` - Args []string `flag:"args" help:"arguments to contract"` -} - -var defaultMakeCallTxOptions = makeCallTxOptions{ - BaseOptions: client.DefaultBaseOptions, - SignBroadcastOptions: defaultSignBroadcastOptions, - PkgPath: "", // must override - Func: "", // must override - Args: nil, - Send: "", -} - -func makeCallTxApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(makeCallTxOptions) - if opts.PkgPath == "" { - return errors.New("pkgpath not specified") - } - if opts.Func == "" { - return errors.New("func not specified") - } - if len(args) != 1 { - cmd.ErrPrintfln("Usage: call ") - return errors.New("invalid args") - } - if opts.GasWanted == 0 { - return errors.New("gas-wanted not specified") - } - if opts.GasFee == "" { - return errors.New("gas-fee not specified") - } - - // read statement. - fnc := opts.Func - - // read account pubkey. - nameOrBech32 := args[0] - kb, err := keys.NewKeyBaseFromDir(opts.Home) - if err != nil { - return err - } - info, err := kb.GetByNameOrAddress(nameOrBech32) - if err != nil { - return err - } - caller := info.GetAddress() - // info.GetPubKey() - - // Parse send amount. - send, err := std.ParseCoins(opts.Send) - if err != nil { - return errors.Wrap(err, "parsing send coins") - } - - // parse gas wanted & fee. - gaswanted := opts.GasWanted - gasfee, err := std.ParseCoin(opts.GasFee) - if err != nil { - return errors.Wrap(err, "parsing gas fee coin") - } - - // construct msg & tx and marshal. - msg := vm.MsgCall{ - Caller: caller, - Send: send, - PkgPath: opts.PkgPath, - Func: fnc, - Args: opts.Args, - } - tx := std.Tx{ - Msgs: []std.Msg{msg}, - Fee: std.NewFee(gaswanted, gasfee), - Signatures: nil, - Memo: opts.Memo, - } - - if opts.Broadcast { - err := signAndBroadcast(cmd, args, tx, opts.BaseOptions, opts.SignBroadcastOptions) - if err != nil { - return err - } - } else { - fmt.Println(string(amino.MustMarshalJSON(tx))) - } - return nil -} - -func signAndBroadcast(cmd *command.Command, args []string, tx std.Tx, baseopts client.BaseOptions, txopts SignBroadcastOptions) error { - // query account - nameOrBech32 := args[0] - kb, err := keys.NewKeyBaseFromDir(baseopts.Home) - if err != nil { - return err - } - info, err := kb.GetByNameOrAddress(nameOrBech32) - if err != nil { - return err - } - accountAddr := info.GetAddress() - - qopts := client.QueryOptions{ - Path: fmt.Sprintf("auth/accounts/%s", accountAddr), - } - qopts.Remote = baseopts.Remote - qres, err := client.QueryHandler(qopts) - if err != nil { - return errors.Wrap(err, "query account") - } - var qret struct{ BaseAccount std.BaseAccount } - err = amino.UnmarshalJSON(qres.Response.Data, &qret) - if err != nil { - return err - } - - // sign tx - accountNumber := qret.BaseAccount.AccountNumber - sequence := qret.BaseAccount.Sequence - sopts := client.SignOptions{ - Sequence: &sequence, - AccountNumber: &accountNumber, - ChainID: txopts.ChainID, - NameOrBech32: nameOrBech32, - TxJSON: amino.MustMarshalJSON(tx), - } - sopts.Home = baseopts.Home - if baseopts.Quiet { - sopts.Pass, err = cmd.GetPassword("", baseopts.InsecurePasswordStdin) - } else { - sopts.Pass, err = cmd.GetPassword("Enter password.", baseopts.InsecurePasswordStdin) - } - if err != nil { - return err - } - - signedTx, err := client.SignHandler(sopts) - if err != nil { - return errors.Wrap(err, "sign tx") - } - - // broadcast signed tx - bopts := client.BroadcastOptions{ - Tx: signedTx, - } - bopts.Remote = baseopts.Remote - bres, err := client.BroadcastHandler(bopts) - if err != nil { - return errors.Wrap(err, "broadcast tx") - } - if bres.CheckTx.IsErr() { - return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) - } - if bres.DeliverTx.IsErr() { - return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) - } - cmd.Println(string(bres.DeliverTx.Data)) - cmd.Println("OK!") - cmd.Println("GAS WANTED:", bres.DeliverTx.GasWanted) - cmd.Println("GAS USED: ", bres.DeliverTx.GasUsed) - - return nil -} - -//---------------------------------------- -// makeSendTxApp - -type makeSendTxOptions struct { - client.BaseOptions // home,... - SignBroadcastOptions // gas-wanted, gas-fee, memo, ... - Send string `flag:"send" help:"send coins"` - To string `flag:"to" help:"destination address"` -} - -var defaultMakeSendTxOptions = makeSendTxOptions{ - BaseOptions: client.DefaultBaseOptions, - SignBroadcastOptions: defaultSignBroadcastOptions, - Send: "", // must override - To: "", // must override -} - -func makeSendTxApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(makeSendTxOptions) - if len(args) != 1 { - cmd.ErrPrintfln("Usage: send ") - return errors.New("invalid args") - } - if opts.GasWanted == 0 { - return errors.New("gas-wanted not specified") - } - if opts.GasFee == "" { - return errors.New("gas-fee not specified") - } - if opts.Send == "" { - return errors.New("send (amount) must be specified") - } - if opts.To == "" { - return errors.New("to (destination address) must be specified") - } - - // read account pubkey. - nameOrBech32 := args[0] - kb, err := keys.NewKeyBaseFromDir(opts.Home) - if err != nil { - return err - } - info, err := kb.GetByNameOrAddress(nameOrBech32) - if err != nil { - return err - } - fromAddr := info.GetAddress() - // info.GetPubKey() - - // Parse to address. - toAddr, err := crypto.AddressFromBech32(opts.To) - if err != nil { - return err - } - - // Parse send amount. - send, err := std.ParseCoins(opts.Send) - if err != nil { - return errors.Wrap(err, "parsing send coins") - } - - // parse gas wanted & fee. - gaswanted := opts.GasWanted - gasfee, err := std.ParseCoin(opts.GasFee) - if err != nil { - return errors.Wrap(err, "parsing gas fee coin") - } - - // construct msg & tx and marshal. - msg := bank.MsgSend{ - FromAddress: fromAddr, - ToAddress: toAddr, - Amount: send, - } - tx := std.Tx{ - Msgs: []std.Msg{msg}, - Fee: std.NewFee(gaswanted, gasfee), - Signatures: nil, - Memo: opts.Memo, - } - - if opts.Broadcast { - err := signAndBroadcast(cmd, args, tx, opts.BaseOptions, opts.SignBroadcastOptions) - if err != nil { - return err - } - } else { - fmt.Println(string(amino.MustMarshalJSON(tx))) - } - return nil -} diff --git a/cmd/gnokey/main.go b/cmd/gnokey/main.go new file mode 100644 index 00000000000..0d77f3f30fe --- /dev/null +++ b/cmd/gnokey/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/gnolang/gno/pkgs/crypto/keys/client" +) + +func main() { + cmd := client.NewRootCmd() + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v", err) + + os.Exit(1) + } +} diff --git a/cmd/gnoland/main.go b/cmd/gnoland/main.go index 53ea04555a1..60302fc4a80 100644 --- a/cmd/gnoland/main.go +++ b/cmd/gnoland/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "fmt" "os" @@ -15,6 +16,7 @@ import ( "github.com/gnolang/gno/pkgs/bft/node" "github.com/gnolang/gno/pkgs/bft/privval" bft "github.com/gnolang/gno/pkgs/bft/types" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto" gno "github.com/gnolang/gno/pkgs/gnolang" "github.com/gnolang/gno/pkgs/log" @@ -23,16 +25,7 @@ import ( "github.com/gnolang/gno/pkgs/std" ) -func main() { - args := os.Args[1:] - err := runMain(args) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) - } -} - -var flags struct { +type gnolandCfg struct { skipFailingGenesisTxs bool skipStart bool genesisBalancesFile string @@ -42,19 +35,82 @@ var flags struct { rootDir string } -func runMain(args []string) error { - fs := flag.NewFlagSet("gnoland", flag.ExitOnError) - fs.BoolVar(&flags.skipFailingGenesisTxs, "skip-failing-genesis-txs", false, "don't panic when replaying invalid genesis txs") - fs.BoolVar(&flags.skipStart, "skip-start", false, "quit after initialization, don't start the node") - fs.StringVar(&flags.genesisBalancesFile, "genesis-balances-file", "./gnoland/genesis/genesis_balances.txt", "initial distribution file") - fs.StringVar(&flags.genesisTxsFile, "genesis-txs-file", "./gnoland/genesis/genesis_txs.txt", "initial txs to replay") - fs.StringVar(&flags.chainID, "chainid", "dev", "chainid") - fs.StringVar(&flags.rootDir, "root-dir", "testdir", "directory for config and data") - fs.StringVar(&flags.genesisRemote, "genesis-remote", "localhost:26657", "replacement for '%%REMOTE%%' in genesis") - fs.Parse(args) +func main() { + cfg := &gnolandCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: "[flags] [...]", + LongHelp: "Starts the gnoland blockchain node", + }, + cfg, + func(_ context.Context, _ []string) error { + return exec(cfg) + }, + ) + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v", err) + + os.Exit(1) + } +} + +func (c *gnolandCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.skipFailingGenesisTxs, + "skip-failing-genesis-txs", + false, + "don't panic when replaying invalid genesis txs", + ) + fs.BoolVar( + &c.skipStart, + "skip-start", + false, + "quit after initialization, don't start the node", + ) + + fs.StringVar( + &c.genesisBalancesFile, + "genesis-balances-file", + "./gnoland/genesis/genesis_balances.txt", + "initial distribution file", + ) + + fs.StringVar( + &c.genesisTxsFile, + "genesis-txs-file", + "./gnoland/genesis/genesis_txs.txt", + "initial txs to replay", + ) + + fs.StringVar( + &c.chainID, + "chainid", + "dev", + "the ID of the chain", + ) + + fs.StringVar( + &c.rootDir, + "root-dir", + "testdir", + "directory for config and data", + ) + + fs.StringVar( + &c.genesisRemote, + "genesis-remote", + "localhost:26657", + "replacement for '%%REMOTE%%' in genesis", + ) +} + +func exec(c *gnolandCfg) error { logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - rootDir := flags.rootDir + rootDir := c.rootDir + cfg := config.LoadOrMakeConfigWithOptions(rootDir, func(cfg *config.Config) { cfg.Consensus.CreateEmptyBlocks = false cfg.Consensus.CreateEmptyBlocksInterval = 60 * time.Second @@ -69,24 +125,33 @@ func runMain(args []string) error { // write genesis file if missing. genesisFilePath := filepath.Join(rootDir, cfg.Genesis) if !osm.FileExists(genesisFilePath) { - genDoc := makeGenesisDoc(priv.GetPubKey()) + genDoc := makeGenesisDoc( + priv.GetPubKey(), + c.chainID, + c.genesisBalancesFile, + loadGenesisTxs(c.genesisTxsFile, c.chainID, c.genesisRemote), + ) writeGenesisFile(genDoc, genesisFilePath) } // create application and node. - gnoApp, err := gnoland.NewApp(rootDir, flags.skipFailingGenesisTxs, logger) + gnoApp, err := gnoland.NewApp(rootDir, c.skipFailingGenesisTxs, logger) if err != nil { return fmt.Errorf("error in creating new app: %w", err) } + cfg.LocalApp = gnoApp + gnoNode, err := node.DefaultNewNode(cfg, logger) if err != nil { return fmt.Errorf("error in creating node: %w", err) } + fmt.Fprintln(os.Stderr, "Node created.") - if flags.skipStart { + if c.skipStart { fmt.Fprintln(os.Stderr, "'--skip-start' is set. Exiting.") + return nil } @@ -100,14 +165,21 @@ func runMain(args []string) error { _ = gnoNode.Stop() } }) + select {} // run forever } // Makes a local test genesis doc with local privValidator. -func makeGenesisDoc(pvPub crypto.PubKey) *bft.GenesisDoc { +func makeGenesisDoc( + pvPub crypto.PubKey, + chainID string, + genesisBalancesFile string, + genesisTxs []std.Tx, +) *bft.GenesisDoc { gen := &bft.GenesisDoc{} + gen.GenesisTime = time.Now() - gen.ChainID = flags.chainID + gen.ChainID = chainID gen.ConsensusParams = abci.ConsensusParams{ Block: &abci.BlockParams{ // TODO: update limits. @@ -127,7 +199,7 @@ func makeGenesisDoc(pvPub crypto.PubKey) *bft.GenesisDoc { } // Load distribution. - balances := loadGenesisBalances(flags.genesisBalancesFile) + balances := loadGenesisBalances(genesisBalancesFile) // debug: for _, balance := range balances { fmt.Println(balance) } // Load initial packages from examples. @@ -171,7 +243,6 @@ func makeGenesisDoc(pvPub crypto.PubKey) *bft.GenesisDoc { } // load genesis txs from file. - genesisTxs := loadGenesisTxs(flags.genesisTxsFile) txs = append(txs, genesisTxs...) // construct genesis AppState. @@ -189,7 +260,11 @@ func writeGenesisFile(gen *bft.GenesisDoc, filePath string) { } } -func loadGenesisTxs(path string) []std.Tx { +func loadGenesisTxs( + path string, + chainID string, + genesisRemote string, +) []std.Tx { txs := []std.Tx{} txsBz := osm.MustReadFile(path) txsLines := strings.Split(string(txsBz), "\n") @@ -199,13 +274,14 @@ func loadGenesisTxs(path string) []std.Tx { } // patch the TX - txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", flags.chainID) - txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", flags.genesisRemote) + txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) + txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) var tx std.Tx amino.MustUnmarshalJSON([]byte(txLine), &tx) txs = append(txs, tx) } + return txs } diff --git a/cmd/gnoland/main_test.go b/cmd/gnoland/main_test.go index 06390be321e..19d170252fb 100644 --- a/cmd/gnoland/main_test.go +++ b/cmd/gnoland/main_test.go @@ -1,11 +1,13 @@ package main import ( + "context" "os" "path/filepath" "strings" "testing" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/testutils" "github.com/stretchr/testify/require" ) @@ -25,7 +27,20 @@ func TestInitialize(t *testing.T) { t.Run(name, func(t *testing.T) { closer := testutils.CaptureStdoutAndStderr() - err := runMain([]string{"--skip-failing-genesis-txs", "--skip-start"}) + cfg := &gnolandCfg{} + cmd := commands.NewCommand( + commands.Metadata{}, + cfg, + func(_ context.Context, _ []string) error { + return exec(cfg) + }, + ) + + err := cmd.ParseAndRun( + context.Background(), + []string{"--skip-failing-genesis-txs", "--skip-start"}, + ) + stdouterr, bufErr := closer() require.NoError(t, bufErr) require.NoError(t, err) diff --git a/cmd/gnotxport/export.go b/cmd/gnotxport/export.go index 4402ac8255e..389df9371fc 100644 --- a/cmd/gnotxport/export.go +++ b/cmd/gnotxport/export.go @@ -1,6 +1,8 @@ package main import ( + "context" + "flag" "fmt" "io" "log" @@ -10,45 +12,66 @@ import ( "github.com/gnolang/gno/pkgs/amino" "github.com/gnolang/gno/pkgs/bft/rpc/client" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/std" - // XXX better way? - _ "github.com/gnolang/gno/pkgs/sdk/auth" + _ "github.com/gnolang/gno/pkgs/sdk/auth" // XXX better way? _ "github.com/gnolang/gno/pkgs/sdk/bank" _ "github.com/gnolang/gno/pkgs/sdk/vm" ) -type txExportOptions struct { - Remote string `flag:"remote" help:"Remote RPC addr:port"` - StartHeight int64 `flag:"start" help:"Start height"` - TailHeight int64 `flag:"tail" help:"Start at LAST - N"` - EndHeight int64 `flag:"end" help:"End height (optional)"` - OutFile string `flag:"out" help:"Output file path"` - Quiet bool `flag:"quiet" help:"Quiet mode"` - Follow bool `flag:"follow" help:"Keep attached and follow new events"` +type exportCfg struct { + rootCfg *config + + startHeight int64 + tailHeight int64 + endHeight int64 + outFile string + quiet bool + follow bool +} + +func newExportCommand(rootCfg *config) *commands.Command { + cfg := &exportCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "export", + ShortUsage: "export [flags] ", + ShortHelp: "Export transactions to file", + }, + cfg, + func(_ context.Context, _ []string) error { + return execExport(cfg) + }, + ) } -var defaultTxExportOptions = txExportOptions{ - Remote: "localhost:26657", - StartHeight: 1, - EndHeight: 0, - TailHeight: 0, - OutFile: "txexport.log", - Quiet: false, - Follow: false, +func (c *exportCfg) RegisterFlags(fs *flag.FlagSet) { + fs.Int64Var(&c.startHeight, "start", 1, "start height") + fs.Int64Var(&c.tailHeight, "tail", 0, "start at LAST - N") + fs.Int64Var(&c.endHeight, "end", 0, "end height (optional)") + fs.StringVar(&c.outFile, "out", defaultFilePath, "output file path") + fs.BoolVar(&c.quiet, "quiet", false, "omit console output during execution") + fs.BoolVar(&c.follow, "follow", false, "keep attached and follow new events") } -func txExportApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(txExportOptions) - c := client.NewHTTP(opts.Remote, "/websocket") - status, err := c.Status() +func execExport(c *exportCfg) error { + node := client.NewHTTP(c.rootCfg.remote, "/websocket") + + status, err := node.Status() if err != nil { - panic(err) + return fmt.Errorf("unable to fetch node status, %w", err) } - start := opts.StartHeight - end := opts.EndHeight - tail := opts.TailHeight + + var ( + start = c.startHeight + end = c.endHeight + tail = c.tailHeight + ) + if end == 0 { // take last block height end = status.SyncInfo.LatestBlockHeight } @@ -57,56 +80,64 @@ func txExportApp(cmd *command.Command, args []string, iopts interface{}) error { } var out io.Writer - switch opts.OutFile { + switch c.outFile { case "-", "STDOUT": out = os.Stdout default: - out, err = os.OpenFile(opts.OutFile, os.O_RDWR|os.O_CREATE, 0o755) + out, err = os.OpenFile(c.outFile, os.O_RDWR|os.O_CREATE, 0o755) if err != nil { return err } } for height := start; ; height++ { - if !opts.Follow && height >= end { + if !c.follow && height >= end { break } getBlock: - block, err := c.Block(&height) + block, err := node.Block(&height) if err != nil { - if opts.Follow && strings.Contains(err.Error(), "") { + if c.follow && strings.Contains(err.Error(), "") { time.Sleep(time.Second) + goto getBlock } - panic(err) + + return fmt.Errorf("encountered error while fetching block, %w", err) } + txs := block.Block.Data.Txs if len(txs) == 0 { continue } - _, err = c.BlockResults(&height) + + _, err = node.BlockResults(&height) if err != nil { - if opts.Follow && strings.Contains(err.Error(), "") { + if c.follow && strings.Contains(err.Error(), "") { time.Sleep(time.Second) + goto getBlock } - panic(err) + + return fmt.Errorf("encountered error while fetching block results, %w", err) } + for i := 0; i < len(txs); i++ { - // need to include error'd txs, to keep sequence alignment. - //if bres.Results.DeliverTxs[i].Error != nil { - // continue - //} tx := txs[i] stdtx := std.Tx{} + amino.MustUnmarshal(tx, &stdtx) + bz := amino.MustMarshalJSON(stdtx) - fmt.Fprintln(out, string(bz)) + + _, _ = fmt.Fprintln(out, string(bz)) } - if !opts.Quiet { + + if !c.quiet { log.Printf("h=%d/%d (txs=%d)", height, end, len(txs)) } } + return nil } diff --git a/cmd/gnotxport/import.go b/cmd/gnotxport/import.go index 1e65e683b10..d35e659389c 100644 --- a/cmd/gnotxport/import.go +++ b/cmd/gnotxport/import.go @@ -1,67 +1,118 @@ package main import ( + "bufio" + "context" + "flag" "fmt" "os" - "strings" "time" "github.com/gnolang/gno/pkgs/amino" "github.com/gnolang/gno/pkgs/bft/rpc/client" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/errors" "github.com/gnolang/gno/pkgs/std" - // XXX better way? - _ "github.com/gnolang/gno/pkgs/sdk/auth" + _ "github.com/gnolang/gno/pkgs/sdk/auth" // XXX better way? _ "github.com/gnolang/gno/pkgs/sdk/bank" _ "github.com/gnolang/gno/pkgs/sdk/vm" ) -type txImportOptions struct { - Remote string `flag:"remote" help:"Remote RPC addr:port"` - InFile string `flag:"in" help:"Input file path"` +type importCfg struct { + rootCfg *config + + inFile string +} + +func newImportCommand(rootCfg *config) *commands.Command { + cfg := &importCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "import", + ShortUsage: "import [flags] ", + ShortHelp: "Import transactions from file", + }, + cfg, + func(ctx context.Context, _ []string) error { + return execImport(ctx, cfg) + }, + ) } -var defaultTxImportOptions = txImportOptions{ - Remote: "localhost:26657", - InFile: "txexport.log", +func (c *importCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar(&c.inFile, "in", defaultFilePath, "input file path") } -func txImportApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(txImportOptions) - c := client.NewHTTP(opts.Remote, "/websocket") - filebz, err := os.ReadFile(opts.InFile) +func execImport(ctx context.Context, c *importCfg) error { + // Initial validation + if len(c.inFile) == 0 { + return errors.New("input file path not specified") + } + + // Read the input file + file, err := os.Open(c.inFile) if err != nil { - return err + return fmt.Errorf("unable to open input file, %w", err) } - lines := strings.Split(strings.TrimSpace(string(filebz)), "\n") - for i, line := range lines { - print(".") - // time.Sleep(10 * time.Second) - if len(line) == 0 { - panic(i) - } - var tx std.Tx - amino.MustUnmarshalJSON([]byte(line), &tx) - txbz := amino.MustMarshal(tx) - res, err := c.BroadcastTxSync(txbz) - if err != nil || res.Error != nil { - print("!") - // wait for next block and try again. - // TODO: actually wait 1 block instead of fudging it. - time.Sleep(20 * time.Second) - res, err := c.BroadcastTxSync(txbz) + + defer file.Close() + + // Start the WS connection to the node + node := client.NewHTTP(c.rootCfg.remote, "/websocket") + + index := 0 + scanner := bufio.NewScanner(file) + for scanner.Scan() { + select { + case <-ctx.Done(): + // Stop signal received while parsing + // the import file + return nil + default: + print(".") + + line := scanner.Text() + if len(line) == 0 { + return fmt.Errorf("empty line encountered at %d", index) + } + + var tx std.Tx + amino.MustUnmarshalJSON([]byte(line), &tx) + txbz := amino.MustMarshal(tx) + + res, err := node.BroadcastTxSync(txbz) + if err != nil || res.Error != nil { - if err != nil { - fmt.Println("SECOND ERROR", err) - } else { - fmt.Println("SECOND ERROR!", res.Error) + print("!") + // wait for next block and try again. + // TODO: actually wait 1 block instead of fudging it. + time.Sleep(20 * time.Second) + + res, err := node.BroadcastTxSync(txbz) + if err != nil || res.Error != nil { + if err != nil { + fmt.Println("SECOND ERROR", err) + } else { + fmt.Println("SECOND ERROR!", res.Error) + } + + fmt.Println(line) + + return errors.Wrap(err, "broadcasting tx %d", index) } - fmt.Println(line) - return errors.Wrap(err, "broadcasting tx %d", i) } + + index++ } } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error encountered while reading file, %w", err) + } + return nil } diff --git a/cmd/gnotxport/main.go b/cmd/gnotxport/main.go index 5f51b4d7d0d..8c2e7c86fa5 100644 --- a/cmd/gnotxport/main.go +++ b/cmd/gnotxport/main.go @@ -1,46 +1,52 @@ package main import ( + "context" + "flag" + "fmt" "os" - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/commands" ) -type ( - AppItem = command.AppItem - AppList = command.AppList -) - -var mainApps AppList = []AppItem{ - {txExportApp, "export", "export txs from node", defaultTxExportOptions}, - {txImportApp, "import", "import txs to node", defaultTxImportOptions}, +// config is the shared config for gnotxport, and its subcommands +type config struct { + remote string `default:"localhost:26657"` } -func main() { - cmd := command.NewStdCommand() - args := os.Args[1:] - - // show help message. - if len(args) == 0 || args[0] == "help" || args[0] == "--help" { - cmd.Println("available subcommands:") - for _, appItem := range mainApps { - cmd.Printf(" %s - %s\n", appItem.Name, appItem.Desc) - } - return - } +const ( + defaultFilePath = "txexport.log" +) - // switch on first argument. - for _, appItem := range mainApps { - if appItem.Name == args[0] { - err := cmd.Run(appItem.App, args[1:], appItem.Defaults) - if err != nil { - panic(err) - } - return - } +func main() { + cfg := &config{} + + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + LongHelp: "Exports or imports transactions from the node", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newImportCommand(cfg), + newExportCommand(cfg), + ) + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v", err) + + os.Exit(1) } +} - // unknown app command! - panic(errors.New("unknown command " + args[0])) +func (c *config) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remote, + "remote", + "localhost:26657", + "remote RPC address ", + ) } diff --git a/cmd/goscan/goscan.go b/cmd/goscan/goscan.go index 518695969c3..5a375a1cc3b 100644 --- a/cmd/goscan/goscan.go +++ b/cmd/goscan/goscan.go @@ -1,30 +1,55 @@ package main import ( + "context" + "flag" "fmt" "go/parser" "go/token" "os" "github.com/davecgh/go-spew/spew" + "github.com/gnolang/gno/pkgs/commands" ) func main() { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: "", + LongHelp: "Prints out the imports for a given file's AST", + }, + commands.NewEmptyConfig(), + execScan, + ) + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v", err) + + os.Exit(1) + } +} + +func execScan(_ context.Context, args []string) error { + if len(args) < 1 { + return flag.ErrHelp + } + fset := token.NewFileSet() // positions are relative to fset - filename := os.Args[1] + filename := args[0] bz, err := os.ReadFile(filename) if err != nil { - panic(err) + return fmt.Errorf("unable to read file, %w", err) } // Parse src but stop after processing the imports. f, err := parser.ParseFile(fset, "", string(bz), parser.ParseComments|parser.DeclarationErrors) if err != nil { - fmt.Println(err) - return + return fmt.Errorf("unable to parse file, %w", err) } // Print the imports from the file's AST. spew.Dump(f) + + return nil } diff --git a/go.mod b/go.mod index 11007887c97..bb51a21f2d4 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/linxGnu/grocksdb v1.7.2 github.com/mattn/go-runewidth v0.0.14 github.com/pelletier/go-toml v1.9.5 + github.com/peterbourgon/ff/v3 v3.3.0 github.com/stretchr/testify v1.8.1 github.com/syndtr/goleveldb v1.0.0 github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c diff --git a/go.sum b/go.sum index 6a9abca9c48..1e61b5861f5 100644 --- a/go.sum +++ b/go.sum @@ -161,6 +161,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24= +github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -306,8 +308,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkgs/command/app.go b/pkgs/command/app.go deleted file mode 100644 index be264144797..00000000000 --- a/pkgs/command/app.go +++ /dev/null @@ -1,11 +0,0 @@ -package command - -type AppItem struct { - App App - Name string // arg name - Desc string // short (single line) description of app - Defaults interface{} // default options - // Help string // long form help -} - -type AppList []AppItem diff --git a/pkgs/command/command.go b/pkgs/command/command.go deleted file mode 100644 index 99789f3da17..00000000000 --- a/pkgs/command/command.go +++ /dev/null @@ -1,153 +0,0 @@ -package command - -import ( - "bufio" - "bytes" - "fmt" - "io" - "os" - "reflect" - "strings" - - "github.com/gnolang/gno/pkgs/amino" -) - -type Command struct { - In io.Reader - InBuf *bufio.Reader - Out io.WriteCloser - OutBuf *bufio.Writer - Err io.WriteCloser - ErrBuf *bufio.Writer - Error error - Flags map[string]interface{} -} - -func NewStdCommand() *Command { - cmd := new(Command) - cmd.SetIn(os.Stdin) // needed for **** GetPassword(). - cmd.SetOut(os.Stdout) - cmd.SetErr(os.Stderr) - return cmd -} - -// An App does something with the *Command inputs and outputs. -// cmd: Command context. -// args: args to app. -// defaults: default options to app. -type App func(cmd *Command, args []string, opts interface{}) error - -// defaults must be supplied for terminal apps only. -// NOTE: defaults is first copied, if provided. -func (cmd *Command) Run(app App, args []string, defaults interface{}) error { - if defaults == nil { - // for root/multi apps. - return app(cmd, args, nil) - } else { - // for terminal apps. - args, flags := ParseArgs(args) - if help, ok := flags["help"]; ok && help == "y" { - // print help. - rv := reflect.ValueOf(defaults) - cmd.printHelpFromDefaults(rv) - return nil - } - // store raw flags. - cmd.Flags = flags - // apply flags to defaults. - ptr := amino.DeepCopyToPtr(defaults) - err := applyFlags(ptr, flags) - if err != nil { - return err - } - opts := reflect.ValueOf(ptr).Elem().Interface() - return app(cmd, args, opts) - } -} - -func (cmd *Command) printHelpFromDefaults(rv reflect.Value) { - rt := rv.Type() - num := rt.NumField() - - // print anonymous embedded struct options - for i := 0; i < num; i++ { - rvf := rv.Field(i) - rtf := rt.Field(i) - if rtf.Anonymous { - cmd.printHelpFromDefaults(rvf) - cmd.Println("") - } else { - continue - } - } - - // print remaining options - cmd.Println("#", rt.Name(), "options") - for i := 0; i < num; i++ { - rvf := rv.Field(i) - rtf := rt.Field(i) - ffn := rtf.Tag.Get("flag") - if rtf.Anonymous { - continue - } else if ffn == "" || ffn == "-" { - // ignore fields with no flags field. - } else { - def := "" - if !rvf.IsZero() { - def = "(default " + fmt.Sprintf("%v", rvf.Interface()) + ") " - } - frt := rtf.Type - help := rtf.Tag.Get("help") - cmd.Println("-", ffn, "("+frt.String()+")", "-", help, def) - } - } -} - -func (cmd *Command) SetIn(in io.Reader) { - cmd.In = in - if inbuf, ok := cmd.In.(*bufio.Reader); ok { - cmd.InBuf = inbuf - } else { - cmd.InBuf = bufio.NewReader(in) - } -} - -func (cmd *Command) SetOut(out io.WriteCloser) { - cmd.Out = out - cmd.OutBuf = bufio.NewWriter(cmd.Out) -} - -func (cmd *Command) SetErr(err io.WriteCloser) { - cmd.Err = err - cmd.ErrBuf = bufio.NewWriter(cmd.Err) -} - -func (cmd *Command) HasFlag(name string) bool { - _, ok := cmd.Flags[name] - return ok -} - -//---------------------------------------- -// NewMockCommand - -// NewMockCommand returns a mock command for testing. -func NewMockCommand() *Command { - mockIn := strings.NewReader("") - mockOut := bytes.NewBufferString("") - mockErr := bytes.NewBufferString("") - cmd := new(Command) - cmd.SetIn(mockIn) - cmd.SetOut(WriteNopCloser(mockOut)) - cmd.SetErr(WriteNopCloser(mockErr)) - return cmd -} - -type writeNopCloser struct { - io.Writer -} - -func (writeNopCloser) Close() error { return nil } - -func WriteNopCloser(w io.Writer) io.WriteCloser { - return writeNopCloser{w} -} diff --git a/pkgs/command/flags.go b/pkgs/command/flags.go deleted file mode 100644 index 68ba275c0e0..00000000000 --- a/pkgs/command/flags.go +++ /dev/null @@ -1,417 +0,0 @@ -package command - -import ( - "encoding/hex" - "fmt" - "os" - "reflect" - "regexp" - "strconv" - "strings" - "time" - - "github.com/gnolang/gno/pkgs/errors" -) - -var reFlagName = regexp.MustCompile(`^--[a-z0-9.\-]+(#[a-z0-9.\-]+)?$`) - -// applies all flags to ptr to options. -// --flag is short for --flag true for boolean flags. -// consecutive flags can be used to populate arrays or slices. -// alternatively, a comma on a single flag can be used. -func applyFlags(ptr interface{}, flags map[string]interface{}) error { - prv := reflect.ValueOf(ptr) - if prv.Type().Kind() != reflect.Ptr { - panic("expected pointer kind to option") - } - rv := prv.Elem() - return applyFlagsReflect(rv, flags) -} - -// apply all flags or return error. -func applyFlagsReflect(rv reflect.Value, flags map[string]interface{}) error { - for fname, fvalue := range flags { - match, err := applyFlagReflect(rv, fname, fvalue) - if err != nil { - return err - } else if match { - continue - } else { - return errors.New("no field found with flag name %s", fname) - } - } - // all matched, return no error. - return nil -} - -// apply flag with name fname to struct or field. -func applyFlagReflect(rv reflect.Value, fname string, fvalue interface{}) (bool, error) { - // scan all fields to find match. - // NOTE inefficient. - // TODO cache/index fields by name. - rt := rv.Type() - num := rv.NumField() - for i := 0; i < num; i++ { - rtf := rt.Field(i) - ffn := rtf.Tag.Get("flag") - if rtf.Anonymous { - // try to match, otherwise continue with other fields. - frv := rv.Field(i) - match, err := applyFlagReflect(frv, fname, fvalue) - if err != nil { - return false, err - } else if match { - // found match, done! - return true, nil - } else { - // continue - } - } else if ffn == "" { - // ignore fields with no flags field. - // NOTE: instead of returning an error here, - // check all structs for consistency beforehand instead. - // Otherwise it's "offensive" programming. - fmt.Fprintf(os.Stderr, "WARN: non-anonymous option field found (%s) with no flag name; in the future this will panic at start of program\n", rtf.Name) - } else if ffn == fname { - frv := rv.Field(i) - return true, applyFlagToFieldReflect(frv, fvalue) - } - } - return false, nil -} - -// apply flag value to a matched field. -func applyFlagToFieldReflect(frv reflect.Value, fvalue interface{}) error { - switch cfvalue := fvalue.(type) { - case map[string]interface{}: - if frv.Type().Kind() != reflect.Struct { - return errors.New( - "expected struct kind but got %v", - frv.Type()) - } - return applyFlagsReflect(frv, cfvalue) - case string: - return applyFlagToFieldReflectString(frv, cfvalue) - case []string: - return applyFlagToFieldReflectStringSlice(frv, cfvalue) - default: - panic("should not happen") - } -} - -// apply flag value string to a matched field. -func applyFlagToFieldReflectString(frv reflect.Value, fvalue string) error { - frt := frv.Type() - switch frt.Kind() { - case reflect.Ptr: - if frv.IsNil() { - frv.Set(reflect.New(frt.Elem())) - } - err := applyFlagToFieldReflectString(frv.Elem(), fvalue) - return err - case reflect.Array: - ert := frt.Elem() - if ert.Kind() == reflect.Uint8 { - bz, err := hex.DecodeString(fvalue) - if err != nil { - // if not hex, try to use the fvalue directly. - bz = []byte(fvalue) - // return errors.Wrap(err, "invalid hex") - } - frv.SetBytes(bz) - return nil - } else { - parts := strings.Split(fvalue, ",") - for i, part := range parts { - erv := frv.Index(i) - err := applyFlagToFieldReflectString(erv, part) - if err != nil { - return errors.Wrap(err, "error parsing item") - } - } - return nil - } - case reflect.Slice: - ert := frt.Elem() - if ert.Kind() == reflect.Uint8 { - bz, err := hex.DecodeString(fvalue) - if err != nil { - // if not hex, try to use the fvalue directly. - bz = []byte(fvalue) - // return errors.Wrap(err, "invalid hex") - } - frv.SetBytes(bz) - return nil - } else { - parts := strings.Split(fvalue, ",") - srv := reflect.MakeSlice(frt, len(parts), len(parts)) - frv.Set(srv) - for i, part := range parts { - erv := frv.Index(i) - err := applyFlagToFieldReflectString(erv, part) - if err != nil { - return errors.Wrap(err, "error parsing item") - } - } - return nil - } - case reflect.Int: - fnum, err := strconv.ParseInt(fvalue, 0, 0) - if err != nil { - return errors.Wrap(err, "invalid int") - } - frv.SetInt(fnum) - return nil - case reflect.Int8: - fnum, err := strconv.ParseInt(fvalue, 0, 8) - if err != nil { - return errors.Wrap(err, "invalid int8") - } - frv.SetInt(fnum) - return nil - case reflect.Int16: - fnum, err := strconv.ParseInt(fvalue, 0, 16) - if err != nil { - return errors.Wrap(err, "invalid int16") - } - frv.SetInt(fnum) - return nil - case reflect.Int32: - fnum, err := strconv.ParseInt(fvalue, 0, 32) - if err != nil { - return errors.Wrap(err, "invalid int32") - } - frv.SetInt(fnum) - return nil - case reflect.Int64: - fnum, err := strconv.ParseInt(fvalue, 0, 64) - if err != nil { - if frt == reflect.TypeOf(time.Duration(0)) { - duration, err := time.ParseDuration(fvalue) - if err != nil { - return errors.Wrap(err, "invalid duration") - } - frv.Set(reflect.ValueOf(duration)) - return nil - } - return errors.Wrap(err, "invalid int64") - } - frv.SetInt(fnum) - return nil - case reflect.Uint: - fnum, err := strconv.ParseUint(fvalue, 0, 0) - if err != nil { - return errors.Wrap(err, "invalid uint") - } - frv.SetUint(fnum) - return nil - case reflect.Uint8: - fnum, err := strconv.ParseUint(fvalue, 0, 8) - if err != nil { - return errors.Wrap(err, "invalid uint8") - } - frv.SetUint(fnum) - return nil - case reflect.Uint16: - fnum, err := strconv.ParseUint(fvalue, 0, 16) - if err != nil { - return errors.Wrap(err, "invalid uint16") - } - frv.SetUint(fnum) - return nil - case reflect.Uint32: - fnum, err := strconv.ParseUint(fvalue, 0, 32) - if err != nil { - return errors.Wrap(err, "invalid uint32") - } - frv.SetUint(fnum) - return nil - case reflect.Uint64: - fnum, err := strconv.ParseUint(fvalue, 0, 64) - if err != nil { - return errors.Wrap(err, "invalid uint64") - } - frv.SetUint(fnum) - return nil - case reflect.String: - // XXX is there something wrong with os.Args? why does it strip '/", and then not unescape \n and \t while unescaping \\? - fvalue = strings.ReplaceAll(fvalue, `\n`, "\n") - fvalue = strings.ReplaceAll(fvalue, `\t`, "\t") - frv.SetString(fvalue) - return nil - case reflect.Bool: - switch fvalue { - case "true", "True", "yes", "Yes", "y", "Y": - frv.SetBool(true) - return nil - case "false", "False", "no", "No", "n", "N": - frv.SetBool(false) - return nil - default: - return errors.New("unexpected bool value: " + fvalue) - } - case reflect.Struct: - panic("not yet implemented") - default: - panic(fmt.Sprintf( - "flag value cannot be applied to field of type %s", - frt.String())) - } -} - -func applyFlagToFieldReflectStringSlice(frv reflect.Value, fvalues []string) error { - frt := frv.Type() - switch frt.Kind() { - case reflect.Array: - for i, part := range fvalues { - erv := frv.Index(i) - err := applyFlagToFieldReflectString(erv, part) - if err != nil { - return errors.Wrap(err, "error parsing item") - } - } - return nil - case reflect.Slice: - srv := reflect.MakeSlice(frt, len(fvalues), len(fvalues)) - frv.Set(srv) - for i, part := range fvalues { - erv := frv.Index(i) - err := applyFlagToFieldReflectString(erv, part) - if err != nil { - return errors.Wrap(err, "error parsing item") - } - } - return nil - default: - panic(fmt.Sprintf( - "flag values cannot be applied to field of type %s", - frt.String())) - } -} - -// all flags follow non-flag args. -func ParseArgs(oargs []string) (args []string, flags map[string]interface{}) { - for i, arg := range oargs { - if strings.HasPrefix(arg, "-") { - args = oargs[:i] - flags = parseFlags(oargs[i:]) - return - } - } - args = oargs - flags = nil - return -} - -func parseFlags(fargs []string) map[string]interface{} { - if len(fargs) == 0 { - return nil - } - m := make(map[string]interface{}, len(fargs)) - var fnamePrev string // for keeping track of repeated flags. - var fname string - for _, farg := range fargs { - if strings.HasPrefix(farg, "--") { - if fname != "" { - // is --flag shortform (like --flag true). - // this cannot happen with repeated flags. - if fnamePrev == fname { - panic(fmt.Sprintf( - "repeated flags cannot include implicit true boolean")) - } - // this cannot happen with file flags. - if strings.HasSuffix(fname, "#file") { - panic(fmt.Sprintf( - "file name not provided for " + fname)) - } - // set y for yes. - setFlag(m, fname, "y", false) - } - fname = parseFlagName(farg) - } else { - if fname == "" { - panic(fmt.Sprintf( - "dangling flag value in args: %s", - farg)) - } - // if a --flag#file flag, read contents. - if strings.HasSuffix(fname, "#file") { - ffile := farg - fargbz, err := os.ReadFile(ffile) - if err != nil { - panic(fmt.Sprintf( - "error reading file: %v", err)) - } - // update fname and farg. - fname = fname[:len(fname)-len("#file")] - farg = string(fargbz) - } - repeat := fname == fnamePrev - setFlag(m, fname, farg, repeat) - fnamePrev = fname // remember - fname = "" // reset - } - } - if fname != "" { - // trailing --fname - repeat := fname == fnamePrev - setFlag(m, fname, "y", repeat) // y for yes - } - return m -} - -// Set the flag value of a key identified by fname to m. -// If fname contains a dot, m will contain a nested map. -// If repeat is true, fvalue will be appended to a slice of existing arg(s). -// Otherwise, panics when encountering a pre-existing flag. -func setFlag(m map[string]interface{}, fname string, fvalue string, repeat bool) { - parts := strings.Split(fname, ".") - setFlagWithParts(m, fname, parts, fvalue, repeat) -} - -// fname: the original flag name. -func setFlagWithParts(m map[string]interface{}, fname string, fparts []string, fvalue string, repeat bool) { - if len(fparts) > 1 { - first := fparts[0] - if m2i, ok := m[first]; ok { - m2 := m2i.(map[string]interface{}) - setFlagWithParts(m2, fname, fparts[1:], fvalue, repeat) - } else { - m2 := make(map[string]interface{}) - setFlagWithParts(m2, fname, fparts[1:], fvalue, repeat) - m[first] = m2 - } - } else { - name := fparts[0] - if !repeat { - if _, exists := m[name]; exists { - panic(fmt.Sprintf( - "flag already set: %s (and repeated flags must be consecutive)", fname)) - } - m[name] = fvalue - } else { - fvaluePrev, exists := m[name] - if !exists { - panic("should not happen") - } - switch fvaluePrev.(type) { - case string: - m[name] = []string{fvaluePrev.(string), fvalue} - case []string: - m[name] = append(fvaluePrev.([]string), fvalue) - default: - panic("should not happen") - } - } - } -} - -func parseFlagName(farg string) string { - match := reFlagName.MatchString(farg) - if !match { - panic(fmt.Sprintf( - "invalid flag name: %s", - farg)) - } - return farg[2:] -} diff --git a/pkgs/command/flags_test.go b/pkgs/command/flags_test.go deleted file mode 100644 index 9f2ff3c5618..00000000000 --- a/pkgs/command/flags_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package command - -import ( - "reflect" - "testing" - "time" -) - -func TestApplyFlagToFieldReflectString(t *testing.T) { - t.Parallel() - - // Test 1: time.Duration - duration := time.Duration(0) - field := reflect.ValueOf(&duration) - flagValue := "10s" - err := applyFlagToFieldReflectString(field, flagValue) - if err != nil { - t.Fatal(err) - } - if duration.String() != flagValue { - t.Errorf("Expected %s, got %s", flagValue, duration) - } -} diff --git a/pkgs/command/prompt.go b/pkgs/command/prompt.go deleted file mode 100644 index fc20276ed07..00000000000 --- a/pkgs/command/prompt.go +++ /dev/null @@ -1,153 +0,0 @@ -package command - -import ( - "errors" - "fmt" - "os" - "strings" - "syscall" - - "golang.org/x/term" -) - -// GetPassword will prompt for a password one-time (to sign a tx). -// Passwords may be blank; user must validate. -func (cmd *Command) GetPassword(prompt string, insecureUseStdin bool) (pass string, err error) { - if prompt != "" { - // On stderr so it isn't part of bash output. - cmd.ErrPrintln(prompt) - } - - // insecure stdin. - if insecureUseStdin { - return cmd.readLineFromInBuf() - } - - // secure prompt. - pass, err = cmd.readPasswordFromInBuf() - if err != nil { - return "", err - } - return pass, nil -} - -// GetCheckPassword will prompt for a password twice to verify they -// match (for creating a new password). -// It enforces the password length. Only parses password once if -// input is piped in. -func (cmd *Command) GetCheckPassword(prompt, prompt2 string) (string, error) { - pass, err := cmd.GetPassword(prompt, false) - if err != nil { - return "", err - } - pass2, err := cmd.GetPassword(prompt2, false) - if err != nil { - return "", err - } - if pass != pass2 { - return "", errors.New("passphrases don't match") - } - return pass, nil -} - -// GetConfirmation will request user give the confirmation from stdin. -// "y", "Y", "yes", "YES", and "Yes" all count as confirmations. -// If the input is not recognized, it returns false and a nil error. -func (cmd *Command) GetConfirmation(prompt string) (bool, error) { - // On stderr so it isn't part of bash output. - cmd.ErrPrintfln("%s [y/n]:", prompt) - - response, err := cmd.readLineFromInBuf() - if err != nil { - return false, err - } - - response = strings.TrimSpace(response) - if len(response) == 0 { - return false, nil - } - - response = strings.ToLower(response) - if response[0] == 'y' { - return true, nil - } - - return false, nil -} - -// GetString simply returns the trimmed string output of a given reader. -func (cmd *Command) GetString(prompt string) (string, error) { - if prompt != "" { - // On stderr so it isn't part of bash output. - cmd.ErrPrintln(prompt) - } - - out, err := cmd.readLineFromInBuf() - if err != nil { - return "", err - } - return strings.TrimSpace(out), nil -} - -// readLineFromInBuf reads one line from stdin. -// Subsequent calls reuse the same buffer, so we don't lose -// any input when reading a password twice (to verify) -func (cmd *Command) readLineFromInBuf() (string, error) { - pass, err := cmd.InBuf.ReadString('\n') - if err != nil { - return "", err - } - return pass[:len(pass)-1], nil -} - -func (cmd *Command) readPasswordFromInBuf() (string, error) { - var fd int - var pass string - if cmd.In == os.Stdin { - fd = syscall.Stdin - inputPass, err := term.ReadPassword(fd) - if err != nil { - return "", err - } - pass = string(inputPass) - } else { - s, err := cmd.InBuf.ReadString('\n') - if err != nil { - return "", err - } - pass = s[:len(s)-1] - } - - return pass, nil -} - -// Println prints a line terminated by a newline. -func (cmd *Command) Println(args ...interface{}) { - fmt.Fprintln(cmd.OutBuf, args...) - cmd.OutBuf.Flush() -} - -// Printf prints a formatted string without trailing newline. -func (cmd *Command) Printf(format string, args ...interface{}) { - fmt.Fprintf(cmd.OutBuf, format, args...) - cmd.OutBuf.Flush() -} - -// Printfln prints a formatted string terminated by a newline. -func (cmd *Command) Printfln(format string, args ...interface{}) { - fmt.Fprintf(cmd.OutBuf, format+"\n", args...) - cmd.OutBuf.Flush() -} - -// ErrPrintln prints a line terminated by a newline to -// cmd.Err(Buf). -func (cmd *Command) ErrPrintln(args ...interface{}) { - fmt.Fprintln(cmd.ErrBuf, args...) - cmd.ErrBuf.Flush() -} - -// ErrPrintfln prints a formatted string terminated by a newline to cmd.Err(Buf). -func (cmd *Command) ErrPrintfln(format string, args ...interface{}) { - fmt.Fprintf(cmd.ErrBuf, format+"\n", args...) - cmd.ErrBuf.Flush() -} diff --git a/pkgs/commands/command.go b/pkgs/commands/command.go new file mode 100644 index 00000000000..d8c40988dab --- /dev/null +++ b/pkgs/commands/command.go @@ -0,0 +1,83 @@ +package commands + +import ( + "context" + "flag" + + "github.com/peterbourgon/ff/v3" + "github.com/peterbourgon/ff/v3/ffcli" +) + +// Config defines the command config interface +// that holds flag values and execution logic +type Config interface { + // RegisterFlags registers the specific flags to the flagset + RegisterFlags(*flag.FlagSet) +} + +// ExecMethod executes the command using the specified config +type ExecMethod func(ctx context.Context, args []string) error + +// HelpExec is a standard exec method for displaying +// help information about a command +func HelpExec(_ context.Context, _ []string) error { + return flag.ErrHelp +} + +// Metadata contains basic help +// information about a command +type Metadata struct { + Name string + ShortUsage string + ShortHelp string + LongHelp string + Options []ff.Option +} + +// Command is a simple wrapper for gnoland +// commands that utilizes ffcli +type Command struct { + ffcli.Command + + cfg Config +} + +func NewCommand( + meta Metadata, + config Config, + exec ExecMethod, +) *Command { + command := &Command{ + Command: ffcli.Command{ + Name: meta.Name, + ShortHelp: meta.ShortHelp, + LongHelp: meta.LongHelp, + ShortUsage: meta.ShortUsage, + Options: meta.Options, + FlagSet: flag.NewFlagSet(meta.Name, flag.ExitOnError), + Exec: exec, + }, + cfg: config, + } + + if config != nil { + // Register the base command flags + config.RegisterFlags(command.FlagSet) + } + + return command +} + +// 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 { + // Register the parent flagset + c.cfg.RegisterFlags(cmd.FlagSet) + } + + // Append the subcommand to the parent + c.Subcommands = append(c.Subcommands, &cmd.Command) + } +} diff --git a/pkgs/commands/empty.go b/pkgs/commands/empty.go new file mode 100644 index 00000000000..fa41f398d2e --- /dev/null +++ b/pkgs/commands/empty.go @@ -0,0 +1,16 @@ +package commands + +import "flag" + +// EmptyConfig is an empty command configuration +// that should be substituted in commands that require one +type EmptyConfig struct { +} + +// NewEmptyConfig creates a new instance of the empty configuration +func NewEmptyConfig() *EmptyConfig { + return &EmptyConfig{} +} + +// RegisterFlags ignores flag set registration +func (ec *EmptyConfig) RegisterFlags(_ *flag.FlagSet) {} diff --git a/pkgs/commands/io.go b/pkgs/commands/io.go new file mode 100644 index 00000000000..b23455e4602 --- /dev/null +++ b/pkgs/commands/io.go @@ -0,0 +1,127 @@ +package commands + +import ( + "bufio" + "fmt" + "io" + "os" +) + +// IO holds settable command +// input, output and error buffers +type IO struct { + In io.Reader + inBuf *bufio.Reader + + Out io.WriteCloser + outBuf *bufio.Writer + + Err io.WriteCloser + errBuf *bufio.Writer +} + +// NewDefaultIO returns a default command io +// that utilizes standard input / output / error +func NewDefaultIO() *IO { + c := &IO{} + + c.SetIn(os.Stdin) + c.SetOut(os.Stdout) + c.SetErr(os.Stderr) + + return c +} + +// NewTestIO returns a test command io +// that only sets standard input (to avoid panics) +func NewTestIO() *IO { + c := &IO{} + c.SetIn(os.Stdin) + + return c +} + +// SetIn sets the input reader for the command io +func (io *IO) SetIn(in io.Reader) { + io.In = in + if inbuf, ok := io.In.(*bufio.Reader); ok { + io.inBuf = inbuf + + return + } + + io.inBuf = bufio.NewReader(in) +} + +// SetOut sets the output writer for the command io +func (io *IO) SetOut(out io.WriteCloser) { + io.Out = out + io.outBuf = bufio.NewWriter(io.Out) +} + +// SetErr sets the error writer for the command io +func (io *IO) SetErr(err io.WriteCloser) { + io.Err = err + io.errBuf = bufio.NewWriter(io.Err) +} + +// Println prints a line terminated by a newline +func (io *IO) Println(args ...interface{}) { + if io.outBuf == nil { + return + } + + _, _ = fmt.Fprintln(io.outBuf, args...) + _ = io.outBuf.Flush() +} + +// Printf prints a formatted string without trailing newline +func (io *IO) Printf(format string, args ...interface{}) { + if io.outBuf == nil { + return + } + + _, _ = fmt.Fprintf(io.outBuf, format, args...) + _ = io.outBuf.Flush() +} + +// Printfln prints a formatted string terminated by a newline +func (io *IO) Printfln(format string, args ...interface{}) { + if io.outBuf == nil { + return + } + + _, _ = fmt.Fprintf(io.outBuf, format+"\n", args...) + _ = io.outBuf.Flush() +} + +// ErrPrintln prints a line terminated by a newline to +// cmd.Err(Buf) +func (io *IO) ErrPrintln(args ...interface{}) { + if io.errBuf == nil { + return + } + + _, _ = fmt.Fprintln(io.errBuf, args...) + _ = io.errBuf.Flush() +} + +// ErrPrintfln prints a formatted string terminated by a newline to cmd.Err(Buf) +func (io *IO) ErrPrintfln(format string, args ...interface{}) { + if io.errBuf == nil { + return + } + + _, _ = fmt.Fprintf(io.errBuf, format+"\n", args...) + _ = io.errBuf.Flush() +} + +type writeNopCloser struct { + io.Writer +} + +func (writeNopCloser) Close() error { return nil } + +func WriteNopCloser(w io.Writer) io.WriteCloser { + return writeNopCloser{w} +} diff --git a/pkgs/commands/types.go b/pkgs/commands/types.go new file mode 100644 index 00000000000..dd771030944 --- /dev/null +++ b/pkgs/commands/types.go @@ -0,0 +1,24 @@ +package commands + +import "strings" + +// StringArr defines the custom flag type +// that represents an array of string values +type StringArr []string + +// String is a required output method for the flag +func (s *StringArr) String() string { + if len(*s) <= 0 { + return "..." + } + + return strings.Join(*s, ", ") +} + +// Set is a required output method for the flag. +// This is where our custom type manipulation actually happens +func (s *StringArr) Set(value string) error { + *s = append(*s, value) + + return nil +} diff --git a/pkgs/commands/utils.go b/pkgs/commands/utils.go new file mode 100644 index 00000000000..c7f85bdadce --- /dev/null +++ b/pkgs/commands/utils.go @@ -0,0 +1,115 @@ +package commands + +import ( + "errors" + "strings" + "syscall" + + "golang.org/x/term" +) + +// GetPassword fetches the password using the provided prompt, if any +func (io *IO) GetPassword( + prompt string, + insecure bool, +) (string, error) { + if prompt != "" { + // Print out the prompt + // On stderr, so it isn't part of bash output + io.ErrPrintln(prompt) + } + + if insecure { + return io.readLine() + } + + return readPassword() +} + +// readLine reads a new line from standard input +func (io *IO) readLine() (string, error) { + input, err := io.inBuf.ReadString('\n') + if err != nil { + return "", err + } + + return input[:len(input)-1], nil +} + +// readPassword reads the password from a terminal +// without local echo +func readPassword() (string, error) { + fd := syscall.Stdin + + inputPass, err := term.ReadPassword(fd) + if err != nil { + return "", err + } + + return string(inputPass), nil +} + +// GetConfirmation will request user give the confirmation from stdin. +// "y", "Y", "yes", "YES", and "Yes" all count as confirmations. +// If the input is not recognized, it returns false and a nil error. +func (io *IO) GetConfirmation(prompt string) (bool, error) { + // On stderr so it isn't part of bash output. + io.ErrPrintfln("%s [y/n]:", prompt) + + response, err := io.readLine() + if err != nil { + return false, err + } + + response = strings.TrimSpace(response) + if len(response) == 0 { + return false, nil + } + + response = strings.ToLower(response) + if response[0] == 'y' { + return true, nil + } + + return false, nil +} + +// GetCheckPassword will prompt for a password twice to verify they +// match (for creating a new password). +// It enforces the password length. Only parses password once if +// input is piped in. +func (io *IO) GetCheckPassword( + prompts [2]string, + insecure bool, +) (string, error) { + pass, err := io.GetPassword(prompts[0], insecure) + if err != nil { + return "", err + } + + pass2, err := io.GetPassword(prompts[1], insecure) + if err != nil { + return "", err + } + + if pass != pass2 { + return "", errors.New("passphrases don't match") + } + + return pass, nil +} + +// GetString simply returns the trimmed string output of a given reader. +func (io *IO) GetString(prompt string) (string, error) { + if prompt != "" { + // On stderr so it isn't part of bash output. + io.ErrPrintln(prompt) + } + + out, err := io.readLine() + if err != nil { + return "", err + } + + return strings.TrimSpace(out), nil +} diff --git a/pkgs/crypto/keys/client/add.go b/pkgs/crypto/keys/client/add.go index 479f47319c1..958a43870ae 100644 --- a/pkgs/crypto/keys/client/add.go +++ b/pkgs/crypto/keys/client/add.go @@ -1,34 +1,121 @@ package client import ( + "context" + "errors" + "flag" "fmt" "sort" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto" "github.com/gnolang/gno/pkgs/crypto/bip39" "github.com/gnolang/gno/pkgs/crypto/keys" "github.com/gnolang/gno/pkgs/crypto/multisig" - "github.com/gnolang/gno/pkgs/errors" ) -type AddOptions struct { - BaseOptions - Multisig []string `flag:"multisig" help:"Construct and store a multisig public key (implies --pubkey)"` - MultisigThreshold int `flag:"threshold" help:"K out of N required signatures. For use in conjunction with --multisig"` - NoSort bool `flag:"nosort" help:"Keys passed to --multisig are taken in the order they're supplied"` - PublicKey string `flag:"pubkey" help:"Parse a public key in bech32 format and save it to disk"` - UseLedger bool `flag:"ledger" help:"Store a local reference to a private key on a Ledger device"` - Recover bool `flag:"recover" help:"Provide seed phrase to recover existing key instead of creating"` - NoBackup bool `flag:"nobackup" help:"Don't print out seed phrase (if others are watching the terminal)"` - DryRun bool `flag:"dryrun" help:"Perform action, but don't add key to local keystore"` - Account uint32 `flag:"account" help:"Account number for HD derivation"` - Index uint32 `flag:"index" description:"Address index number for HD derivation"` +type addCfg struct { + rootCfg *baseCfg + + multisig commands.StringArr + multisigThreshold int + noSort bool + publicKey string + useLedger bool + recover bool + noBackup bool + dryRun bool + account uint64 + index uint64 } -var DefaultAddOptions = AddOptions{ - BaseOptions: DefaultBaseOptions, - MultisigThreshold: 1, +func newAddCmd(rootCfg *baseCfg) *commands.Command { + cfg := &addCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "add", + ShortUsage: "add [flags] ", + ShortHelp: "Adds key to the keybase", + }, + cfg, + func(_ context.Context, args []string) error { + return execAdd(cfg, args, commands.NewDefaultIO()) + }, + ) +} + +func (c *addCfg) RegisterFlags(fs *flag.FlagSet) { + fs.Var( + &c.multisig, + "multisig", + "Construct and store a multisig public key (implies --pubkey)", + ) + + fs.IntVar( + &c.multisigThreshold, + "threshold", + 1, + "K out of N required signatures. For use in conjunction with --multisig", + ) + + fs.BoolVar( + &c.noSort, + "nosort", + false, + "Keys passed to --multisig are taken in the order they're supplied", + ) + + fs.StringVar( + &c.publicKey, + "pubkey", + "", + "Parse a public key in bech32 format and save it to disk", + ) + + fs.BoolVar( + &c.useLedger, + "ledger", + false, + "Store a local reference to a private key on a Ledger device", + ) + + fs.BoolVar( + &c.recover, + "recover", + false, + "Provide seed phrase to recover existing key instead of creating", + ) + + fs.BoolVar( + &c.noBackup, + "nobackup", + false, + "Don't print out seed phrase (if others are watching the terminal)", + ) + + fs.BoolVar( + &c.dryRun, + "dryrun", + false, + "Perform action, but don't add key to local keystore", + ) + + fs.Uint64Var( + &c.account, + "account", + 0, + "Account number for HD derivation", + ) + + fs.Uint64Var( + &c.index, + "index", + 0, + "Address index number for HD derivation", + ) } // DryRunKeyPass contains the default key password for genesis transactions @@ -44,27 +131,27 @@ input output - armor encrypted private key (saved to file) */ -func addApp(cmd *command.Command, args []string, iopts interface{}) error { - var kb keys.Keybase - var err error - var encryptPassword string - opts := iopts.(AddOptions) +func execAdd(cfg *addCfg, args []string, io *commands.IO) error { + var ( + kb keys.Keybase + err error + encryptPassword string + ) if len(args) != 1 { - cmd.ErrPrintfln("Usage: add ") - return errors.New("invalid args") + return flag.ErrHelp } name := args[0] - showMnemonic := !opts.NoBackup + showMnemonic := !cfg.noBackup - if opts.DryRun { + if cfg.dryRun { // we throw this away, so don't enforce args, // we want to get a new random seed phrase quickly kb = keys.NewInMemory() encryptPassword = DryRunKeyPass } else { - kb, err = keys.NewKeyBaseFromDir(opts.Home) + kb, err = keys.NewKeyBaseFromDir(cfg.rootCfg.Home) if err != nil { return err } @@ -72,7 +159,7 @@ func addApp(cmd *command.Command, args []string, iopts interface{}) error { _, err = kb.GetByName(name) if err == nil { // account exists, ask for user confirmation - response, err2 := cmd.GetConfirmation(fmt.Sprintf("Override the existing name %s", name)) + response, err2 := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name)) if err2 != nil { return err2 } @@ -81,11 +168,11 @@ func addApp(cmd *command.Command, args []string, iopts interface{}) error { } } - multisigKeys := opts.Multisig + multisigKeys := cfg.multisig if len(multisigKeys) != 0 { var pks []crypto.PubKey - multisigThreshold := opts.MultisigThreshold + multisigThreshold := cfg.multisigThreshold if err := keys.ValidateMultisigThreshold(multisigThreshold, len(multisigKeys)); err != nil { return err } @@ -99,7 +186,7 @@ func addApp(cmd *command.Command, args []string, iopts interface{}) error { } // Handle --nosort - if !opts.NoSort { + if !cfg.noSort { sort.Slice(pks, func(i, j int) bool { return pks[i].Address().Compare(pks[j].Address()) < 0 }) @@ -110,23 +197,27 @@ func addApp(cmd *command.Command, args []string, iopts interface{}) error { return err } - cmd.Printfln("Key %q saved to disk.\n", name) + io.Printfln("Key %q saved to disk.\n", name) return nil } // ask for a password when generating a local key - if opts.PublicKey == "" && !opts.UseLedger { - encryptPassword, err = cmd.GetCheckPassword( - "Enter a passphrase to encrypt your key to disk:", - "Repeat the passphrase:") + if cfg.publicKey == "" && !cfg.useLedger { + encryptPassword, err = io.GetCheckPassword( + [2]string{ + "Enter a passphrase to encrypt your key to disk:", + "Repeat the passphrase:", + }, + cfg.rootCfg.InsecurePasswordStdin, + ) if err != nil { return err } } } - if opts.PublicKey != "" { - pk, err := crypto.PubKeyFromBech32(opts.PublicKey) + if cfg.publicKey != "" { + pk, err := crypto.PubKeyFromBech32(cfg.publicKey) if err != nil { return err } @@ -137,27 +228,27 @@ func addApp(cmd *command.Command, args []string, iopts interface{}) error { return nil } - account := opts.Account - index := opts.Index + account := cfg.account + index := cfg.index // If we're using ledger, only thing we need is the path and the bech32 prefix. - if opts.UseLedger { + if cfg.useLedger { bech32PrefixAddr := crypto.Bech32AddrPrefix - info, err := kb.CreateLedger(name, keys.Secp256k1, bech32PrefixAddr, account, index) + info, err := kb.CreateLedger(name, keys.Secp256k1, bech32PrefixAddr, uint32(account), uint32(index)) if err != nil { return err } - return printCreate(cmd, info, false, "") + return printCreate(info, false, "", io) } // Get bip39 mnemonic var mnemonic string const bip39Passphrase string = "" // XXX research. - if opts.Recover { + if cfg.recover { bip39Message := "Enter your bip39 mnemonic" - mnemonic, err = cmd.GetString(bip39Message) + mnemonic, err = io.GetString(bip39Message) if err != nil { return err } @@ -174,31 +265,30 @@ func addApp(cmd *command.Command, args []string, iopts interface{}) error { } } - info, err := kb.CreateAccount(name, mnemonic, bip39Passphrase, encryptPassword, account, index) + info, err := kb.CreateAccount(name, mnemonic, bip39Passphrase, encryptPassword, uint32(account), uint32(index)) if err != nil { return err } // Recover key from seed passphrase - if opts.Recover { + if cfg.recover { // Hide mnemonic from output showMnemonic = false mnemonic = "" } - return printCreate(cmd, info, showMnemonic, mnemonic) + return printCreate(info, showMnemonic, mnemonic, io) } -func printCreate(cmd *command.Command, info keys.Info, showMnemonic bool, mnemonic string) error { - cmd.Println("") - printNewInfo(cmd, info) +func printCreate(info keys.Info, showMnemonic bool, mnemonic string, io *commands.IO) error { + io.Println("") + printNewInfo(info, io) // print mnemonic unless requested not to. if showMnemonic { - cmd.Printfln(` + io.Printfln(` **IMPORTANT** write this mnemonic phrase in a safe place. It is the only way to recover your account if you ever forget your password. - %v `, mnemonic) } @@ -206,12 +296,13 @@ It is the only way to recover your account if you ever forget your password. return nil } -func printNewInfo(cmd *command.Command, info keys.Info) { +func printNewInfo(info keys.Info, io *commands.IO) { keyname := info.GetName() keytype := info.GetType() keypub := info.GetPubKey() keyaddr := info.GetAddress() keypath, _ := info.GetPath() - cmd.Printfln("* %s (%s) - addr: %v pub: %v, path: %v", + + io.Printfln("* %s (%s) - addr: %v pub: %v, path: %v", keyname, keytype, keyaddr, keypub, keypath) } diff --git a/pkgs/crypto/keys/client/add_ledger_test.go b/pkgs/crypto/keys/client/add_ledger_test.go deleted file mode 100644 index ec7f890ab1a..00000000000 --- a/pkgs/crypto/keys/client/add_ledger_test.go +++ /dev/null @@ -1,107 +0,0 @@ -//go:build ledger || test_ledger_mock -// +build ledger test_ledger_mock - -package client - -/* -import ( - "testing" - - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - - "github.com/tendermint/classic/libs/cli" - - "github.com/tendermint/classic/sdk/client/flags" - "github.com/tendermint/classic/sdk/crypto/keys" - "github.com/tendermint/classic/sdk/tests" - sdk "github.com/tendermint/classic/sdk/types" -) - -func Test_runAddCmdLedgerWithCustomCoinType(t *testing.T) { - config := sdk.GetConfig() - - bech32PrefixAccAddr := "terra" - bech32PrefixAccPub := "terrapub" - bech32PrefixValAddr := "terravaloper" - bech32PrefixValPub := "terravaloperpub" - bech32PrefixConsAddr := "terravalcons" - bech32PrefixConsPub := "terravalconspub" - - config.SetCoinType(330) - config.SetFullFundraiserPath("44'/330'/0'/0/0") - config.SetBech32PrefixForAccount(bech32PrefixAccAddr, bech32PrefixAccPub) - config.SetBech32PrefixForValidator(bech32PrefixValAddr, bech32PrefixValPub) - config.SetBech32PrefixForConsensusNode(bech32PrefixConsAddr, bech32PrefixConsPub) - - cmd := addKeyCommand() - assert.NotNil(t, cmd) - - // Prepare a keybase - kbHome, kbCleanUp := tests.NewTestCaseDir(t) - assert.NotNil(t, kbHome) - defer kbCleanUp() - viper.Set(flags.FlagHome, kbHome) - viper.Set(flags.FlagUseLedger, true) - - /// Test Text - viper.Set(cli.OutputFlag, OutputFormatText) - // Now enter password - mockIn, _, _ := tests.ApplyMockIO(cmd) - mockIn.Reset("test1234\ntest1234\n") - assert.NoError(t, runAddCmd(cmd, []string{"keyname1"})) - - // Now check that it has been stored properly - kb, err := NewKeyBaseFromHomeFlag() - assert.NoError(t, err) - assert.NotNil(t, kb) - key1, err := kb.Get("keyname1") - assert.NoError(t, err) - assert.NotNil(t, key1) - - assert.Equal(t, "keyname1", key1.GetName()) - assert.Equal(t, keys.TypeLedger, key1.GetType()) - assert.Equal(t, - "terrapub1addwnpepqvpg7r26nl2pvqqern00m6s9uaax3hauu2rzg8qpjzq9hy6xve7sw0d84m6", - sdk.MustBech32ifyAccPub(key1.GetPubKey())) - - config.SetCoinType(118) - config.SetFullFundraiserPath("44'/118'/0'/0/0") - config.SetBech32PrefixForAccount(sdk.Bech32PrefixAccAddr, sdk.Bech32PrefixAccPub) - config.SetBech32PrefixForValidator(sdk.Bech32PrefixValAddr, sdk.Bech32PrefixValPub) - config.SetBech32PrefixForConsensusNode(sdk.Bech32PrefixConsAddr, sdk.Bech32PrefixConsPub) -} - -func Test_runAddCmdLedger(t *testing.T) { - cmd := addKeyCommand() - assert.NotNil(t, cmd) - - // Prepare a keybase - kbHome, kbCleanUp := tests.NewTestCaseDir(t) - assert.NotNil(t, kbHome) - defer kbCleanUp() - viper.Set(flags.FlagHome, kbHome) - viper.Set(flags.FlagUseLedger, true) - - /// Test Text - viper.Set(cli.OutputFlag, OutputFormatText) - // Now enter password - mockIn, _, _ := tests.ApplyMockIO(cmd) - mockIn.Reset("test1234\ntest1234\n") - assert.NoError(t, runAddCmd(cmd, []string{"keyname1"})) - - // Now check that it has been stored properly - kb, err := NewKeyBaseFromHomeFlag() - assert.NoError(t, err) - assert.NotNil(t, kb) - key1, err := kb.Get("keyname1") - assert.NoError(t, err) - assert.NotNil(t, key1) - - assert.Equal(t, "keyname1", key1.GetName()) - assert.Equal(t, keys.TypeLedger, key1.GetType()) - assert.Equal(t, - "cosmospub1addwnpepqd87l8xhcnrrtzxnkql7k55ph8fr9jarf4hn6udwukfprlalu8lgw0urza0", - sdk.MustBech32ifyAccPub(key1.GetPubKey())) -} -*/ diff --git a/pkgs/crypto/keys/client/add_test.go b/pkgs/crypto/keys/client/add_test.go index 2a152d16ce2..813d2ebc1c0 100644 --- a/pkgs/crypto/keys/client/add_test.go +++ b/pkgs/crypto/keys/client/add_test.go @@ -5,40 +5,46 @@ import ( "strings" "testing" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" "github.com/gnolang/gno/pkgs/crypto/secp256k1" "github.com/gnolang/gno/pkgs/testutils" - "github.com/jaekwon/testify/assert" + "github.com/stretchr/testify/assert" ) -func Test_addAppBasic(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) +func Test_execAddBasic(t *testing.T) { + t.Parallel() // make new test dir kbHome, kbCleanUp := testutils.NewTestCaseDir(t) assert.NotNil(t, kbHome) defer kbCleanUp() - // initialize test options - opts := AddOptions{ - BaseOptions: BaseOptions{ - Home: kbHome, + cfg := &addCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + }, }, } - cmd.SetIn(strings.NewReader("test1234\ntest1234\n")) - err := addApp(cmd, []string{"keyname1"}, opts) - assert.NoError(t, err) + keyName := "keyname1" - cmd.SetIn(strings.NewReader("test1234\ntest1234\n")) - err = addApp(cmd, []string{"keyname1"}, opts) - assert.Error(t, err) + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) - cmd.SetIn(strings.NewReader("y\ntest1234\ntest1234\n")) - err = addApp(cmd, []string{"keyname1"}, opts) - assert.NoError(t, err) + // Create a new key + if err := execAdd(cfg, []string{keyName}, io); err != nil { + t.Fatalf("unable to execute add cmd, %v", err) + } + + io.SetIn(strings.NewReader("y\ntest1234\ntest1234\n")) + + // Confirm overwrite + if err := execAdd(cfg, []string{keyName}, io); err != nil { + t.Fatalf("unable to execute add cmd, %v", err) + } } var ( @@ -46,50 +52,55 @@ var ( test2PubkeyBech32 = "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqg5y7u93gpzug38k2p8s8322zpdm96t0ch87ax88sre4vnclz2jcy8uyhst" ) -func Test_addPublicKey(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) +func Test_execAddPublicKey(t *testing.T) { + t.Parallel() kbHome, kbCleanUp := testutils.NewTestCaseDir(t) assert.NotNil(t, kbHome) defer kbCleanUp() - opts := AddOptions{ - BaseOptions: BaseOptions{ - Home: kbHome, + cfg := &addCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + Home: kbHome, + }, }, + publicKey: test2PubkeyBech32, // test2 account + } - PublicKey: test2PubkeyBech32, // test2 account + if err := execAdd(cfg, []string{"test2"}, nil); err != nil { + t.Fatalf("unable to execute add cmd, %v", err) } - err := addApp(cmd, []string{"test2"}, opts) - assert.NoError(t, err) } -func Test_addAppRecover(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) +func Test_execAddRecover(t *testing.T) { + t.Parallel() kbHome, kbCleanUp := testutils.NewTestCaseDir(t) assert.NotNil(t, kbHome) defer kbCleanUp() - opts := AddOptions{ - BaseOptions: BaseOptions{ - Home: kbHome, + cfg := &addCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + }, }, - - Recover: true, // init test2 account + recover: true, // init test2 account } test2Name := "test2" test2Passphrase := "gn0rocks!" - cmd.SetIn(strings.NewReader(test2Passphrase + "\n" + test2Passphrase + "\n" + test2Mnemonic + "\n")) + io := commands.NewTestIO() + io.SetIn(strings.NewReader(test2Passphrase + "\n" + test2Passphrase + "\n" + test2Mnemonic + "\n")) - err := addApp(cmd, []string{test2Name}, opts) - assert.NoError(t, err) + if err := execAdd(cfg, []string{test2Name}, io); err != nil { + t.Fatalf("unable to execute add cmd, %v", err) + } - kb, err2 := keys.NewKeyBaseFromDir(opts.Home) + kb, err2 := keys.NewKeyBaseFromDir(kbHome) assert.NoError(t, err2) infos, err3 := kb.List() diff --git a/pkgs/crypto/keys/client/addpkg.go b/pkgs/crypto/keys/client/addpkg.go new file mode 100644 index 00000000000..198549aa876 --- /dev/null +++ b/pkgs/crypto/keys/client/addpkg.go @@ -0,0 +1,217 @@ +package client + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/pkgs/amino" + "github.com/gnolang/gno/pkgs/commands" + "github.com/gnolang/gno/pkgs/crypto/keys" + "github.com/gnolang/gno/pkgs/errors" + gno "github.com/gnolang/gno/pkgs/gnolang" + "github.com/gnolang/gno/pkgs/sdk/vm" + "github.com/gnolang/gno/pkgs/std" +) + +type addPkgCfg struct { + rootCfg *makeTxCfg + + pkgPath string + pkgDir string + deposit string +} + +func newAddPkgCmd(rootCfg *makeTxCfg) *commands.Command { + cfg := &addPkgCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "addpkg", + ShortUsage: "addpkg [flags] ", + ShortHelp: "Uploads a new package", + }, + cfg, + func(_ context.Context, args []string) error { + return execAddPkg(cfg, args, commands.NewDefaultIO()) + }, + ) +} + +func (c *addPkgCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.pkgPath, + "pkgpath", + "", + "package path (required)", + ) + + fs.StringVar( + &c.pkgDir, + "pkgdir", + "", + "path to package files (required)", + ) + + fs.StringVar( + &c.pkgDir, + "deposit", + "", + "deposit coins", + ) +} + +func execAddPkg(cfg *addPkgCfg, args []string, io *commands.IO) error { + if cfg.pkgPath == "" { + return errors.New("pkgpath not specified") + } + if cfg.pkgDir == "" { + return errors.New("pkgdir not specified") + } + + if len(args) != 1 { + return flag.ErrHelp + } + + // read account pubkey. + nameOrBech32 := args[0] + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.rootCfg.Home) + if err != nil { + return err + } + info, err := kb.GetByNameOrAddress(nameOrBech32) + if err != nil { + return err + } + creator := info.GetAddress() + // info.GetPubKey() + + // parse deposit. + deposit, err := std.ParseCoins(cfg.deposit) + if err != nil { + panic(err) + } + + // open files in directory as MemPackage. + memPkg := gno.ReadMemPackage(cfg.pkgDir, cfg.pkgPath) + + // precompile and validate syntax + err = gno.PrecompileAndCheckMempkg(memPkg) + if err != nil { + panic(err) + } + + // parse gas wanted & fee. + gaswanted := cfg.rootCfg.gasWanted + gasfee, err := std.ParseCoin(cfg.rootCfg.gasFee) + if err != nil { + panic(err) + } + // construct msg & tx and marshal. + msg := vm.MsgAddPackage{ + Creator: creator, + Package: memPkg, + Deposit: deposit, + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: cfg.rootCfg.memo, + } + + if cfg.rootCfg.broadcast { + err := signAndBroadcast(cfg.rootCfg, args, tx, io) + if err != nil { + return err + } + } else { + fmt.Println(string(amino.MustMarshalJSON(tx))) + } + return nil +} + +func signAndBroadcast( + cfg *makeTxCfg, + args []string, + tx std.Tx, + io *commands.IO, +) error { + baseopts := cfg.rootCfg + txopts := cfg + + // query account + nameOrBech32 := args[0] + kb, err := keys.NewKeyBaseFromDir(baseopts.Home) + if err != nil { + return err + } + info, err := kb.GetByNameOrAddress(nameOrBech32) + if err != nil { + return err + } + accountAddr := info.GetAddress() + + qopts := &queryCfg{ + path: fmt.Sprintf("auth/accounts/%s", accountAddr), + } + qopts.rootCfg.Remote = baseopts.Remote + qres, err := queryHandler(qopts) + if err != nil { + return errors.Wrap(err, "query account") + } + var qret struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(qres.Response.Data, &qret) + if err != nil { + return err + } + + // sign tx + accountNumber := qret.BaseAccount.AccountNumber + sequence := qret.BaseAccount.Sequence + sopts := &signCfg{ + sequence: sequence, + accountNumber: accountNumber, + chainID: txopts.chainID, + nameOrBech32: nameOrBech32, + txJSON: amino.MustMarshalJSON(tx), + } + sopts.rootCfg.Home = baseopts.Home + if baseopts.Quiet { + sopts.pass, err = io.GetPassword("", baseopts.InsecurePasswordStdin) + } else { + sopts.pass, err = io.GetPassword("Enter password.", baseopts.InsecurePasswordStdin) + } + if err != nil { + return err + } + + signedTx, err := SignHandler(sopts) + if err != nil { + return errors.Wrap(err, "sign tx") + } + + // broadcast signed tx + bopts := &broadcastCfg{ + tx: signedTx, + } + bopts.rootCfg.Remote = baseopts.Remote + bres, err := broadcastHandler(bopts) + if err != nil { + return errors.Wrap(err, "broadcast tx") + } + if bres.CheckTx.IsErr() { + return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + } + if bres.DeliverTx.IsErr() { + return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + } + io.Println(string(bres.DeliverTx.Data)) + io.Println("OK!") + io.Println("GAS WANTED:", bres.DeliverTx.GasWanted) + io.Println("GAS USED: ", bres.DeliverTx.GasUsed) + + return nil +} diff --git a/pkgs/crypto/keys/client/broadcast.go b/pkgs/crypto/keys/client/broadcast.go index 655a98c761f..7eb096f259d 100644 --- a/pkgs/crypto/keys/client/broadcast.go +++ b/pkgs/crypto/keys/client/broadcast.go @@ -1,35 +1,58 @@ package client import ( + "context" + "flag" "os" "github.com/gnolang/gno/pkgs/amino" abci "github.com/gnolang/gno/pkgs/bft/abci/types" "github.com/gnolang/gno/pkgs/bft/rpc/client" ctypes "github.com/gnolang/gno/pkgs/bft/rpc/core/types" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/errors" "github.com/gnolang/gno/pkgs/std" ) -type BroadcastOptions struct { - BaseOptions +type broadcastCfg struct { + rootCfg *baseCfg + + dryRun bool // internal - Tx *std.Tx `flag:"-"` - DryRun bool `flag:"dry-run"` + tx *std.Tx } -var DefaultBroadcastOptions = BroadcastOptions{ - BaseOptions: DefaultBaseOptions, +func newBroadcastCmd(rootCfg *baseCfg) *commands.Command { + cfg := &broadcastCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "broadcast", + ShortUsage: "broadcast [flags] ", + ShortHelp: "Broadcasts a signed document", + }, + nil, + func(_ context.Context, args []string) error { + return execBroadcast(cfg, args, commands.NewDefaultIO()) + }, + ) } -func broadcastApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(BroadcastOptions) +func (c *broadcastCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.dryRun, + "dry-run", + false, + "perform a dry-run broadcast", + ) +} +func execBroadcast(cfg *broadcastCfg, args []string, io *commands.IO) error { if len(args) != 1 { - cmd.ErrPrintfln("Usage: broadcast ") - return errors.New("invalid args") + return flag.ErrHelp } filename := args[0] @@ -42,9 +65,9 @@ func broadcastApp(cmd *command.Command, args []string, iopts interface{}) error if err != nil { return errors.Wrap(err, "unmarshaling tx json bytes") } - opts.Tx = &tx + cfg.tx = &tx - res, err := BroadcastHandler(opts) + res, err := broadcastHandler(cfg) if err != nil { return err } @@ -54,33 +77,33 @@ func broadcastApp(cmd *command.Command, args []string, iopts interface{}) error } else if res.DeliverTx.IsErr() { return errors.New("transaction failed %#v\nlog %s", res, res.DeliverTx.Log) } else { - cmd.Println(string(res.DeliverTx.Data)) - cmd.Println("OK!") - cmd.Println("GAS WANTED:", res.DeliverTx.GasWanted) - cmd.Println("GAS USED: ", res.DeliverTx.GasUsed) + io.Println(string(res.DeliverTx.Data)) + io.Println("OK!") + io.Println("GAS WANTED:", res.DeliverTx.GasWanted) + io.Println("GAS USED: ", res.DeliverTx.GasUsed) } return nil } -func BroadcastHandler(opts BroadcastOptions) (*ctypes.ResultBroadcastTxCommit, error) { - if opts.Tx == nil { +func broadcastHandler(cfg *broadcastCfg) (*ctypes.ResultBroadcastTxCommit, error) { + if cfg.tx == nil { return nil, errors.New("invalid tx") } - remote := opts.Remote + remote := cfg.rootCfg.Remote if remote == "" || remote == "y" { return nil, errors.New("missing remote url") } - bz, err := amino.Marshal(opts.Tx) + bz, err := amino.Marshal(cfg.tx) if err != nil { return nil, errors.Wrap(err, "remarshaling tx binary bytes") } cli := client.NewHTTP(remote, "/websocket") - if opts.DryRun { - return SimulateTx(cli, bz) + if cfg.dryRun { + return simulateTx(cli, bz) } bres, err := cli.BroadcastTxCommit(bz) @@ -91,7 +114,7 @@ func BroadcastHandler(opts BroadcastOptions) (*ctypes.ResultBroadcastTxCommit, e return bres, nil } -func SimulateTx(cli client.ABCIClient, tx []byte) (*ctypes.ResultBroadcastTxCommit, error) { +func simulateTx(cli client.ABCIClient, tx []byte) (*ctypes.ResultBroadcastTxCommit, error) { bres, err := cli.ABCIQuery(".app/simulate", tx) if err != nil { return nil, errors.Wrap(err, "simulate tx") diff --git a/pkgs/crypto/keys/client/call.go b/pkgs/crypto/keys/client/call.go new file mode 100644 index 00000000000..e6473dd541a --- /dev/null +++ b/pkgs/crypto/keys/client/call.go @@ -0,0 +1,142 @@ +package client + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/pkgs/amino" + "github.com/gnolang/gno/pkgs/commands" + "github.com/gnolang/gno/pkgs/crypto/keys" + "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/sdk/vm" + "github.com/gnolang/gno/pkgs/std" +) + +type callCfg struct { + rootCfg *makeTxCfg + + send string + pkgPath string + funcName string + args commands.StringArr +} + +func newCallCmd(rootCfg *makeTxCfg) *commands.Command { + cfg := &callCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "call", + ShortUsage: "call [flags] ", + ShortHelp: "Executes a Realm function call", + }, + cfg, + func(_ context.Context, args []string) error { + return execCall(cfg, args, commands.NewDefaultIO()) + }, + ) +} + +func (c *callCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.send, + "send", + "", + "send amount", + ) + + fs.StringVar( + &c.pkgPath, + "pkgpath", + "", + "package path (required)", + ) + + fs.StringVar( + &c.funcName, + "func", + "", + "contract to call (required)", + ) + + fs.Var( + &c.args, + "args", + "arguments to contract", + ) +} + +func execCall(cfg *callCfg, args []string, io *commands.IO) error { + if cfg.pkgPath == "" { + return errors.New("pkgpath not specified") + } + if cfg.funcName == "" { + return errors.New("func not specified") + } + if len(args) != 1 { + return flag.ErrHelp + } + if cfg.rootCfg.gasWanted == 0 { + return errors.New("gas-wanted not specified") + } + if cfg.rootCfg.gasFee == "" { + return errors.New("gas-fee not specified") + } + + // read statement. + fnc := cfg.funcName + + // read account pubkey. + nameOrBech32 := args[0] + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.rootCfg.Home) + if err != nil { + return err + } + info, err := kb.GetByNameOrAddress(nameOrBech32) + if err != nil { + return err + } + caller := info.GetAddress() + // info.GetPubKey() + + // Parse send amount. + send, err := std.ParseCoins(cfg.send) + if err != nil { + return errors.Wrap(err, "parsing send coins") + } + + // parse gas wanted & fee. + gaswanted := cfg.rootCfg.gasWanted + gasfee, err := std.ParseCoin(cfg.rootCfg.gasFee) + if err != nil { + return errors.Wrap(err, "parsing gas fee coin") + } + + // construct msg & tx and marshal. + msg := vm.MsgCall{ + Caller: caller, + Send: send, + PkgPath: cfg.pkgPath, + Func: fnc, + Args: cfg.args, + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: cfg.rootCfg.memo, + } + + if cfg.rootCfg.broadcast { + err := signAndBroadcast(cfg.rootCfg, args, tx, io) + if err != nil { + return err + } + } else { + fmt.Println(string(amino.MustMarshalJSON(tx))) + } + return nil +} diff --git a/pkgs/crypto/keys/client/options.go b/pkgs/crypto/keys/client/common.go similarity index 60% rename from pkgs/crypto/keys/client/options.go rename to pkgs/crypto/keys/client/common.go index 469f5b9f95a..cf070ce5040 100644 --- a/pkgs/crypto/keys/client/options.go +++ b/pkgs/crypto/keys/client/common.go @@ -6,10 +6,10 @@ import ( ) type BaseOptions struct { - Home string `flag:"home" help:"home directory"` - Remote string `flag:"remote" help:"remote node URL (default 127.0.0.1:26657)"` - Quiet bool `flag:"quiet" help:"for parsing output"` - InsecurePasswordStdin bool `flag:"insecure-password-stdin" help:"WARNING! take password from stdin"` + Home string + Remote string + Quiet bool + InsecurePasswordStdin bool } var DefaultBaseOptions = BaseOptions{ diff --git a/pkgs/crypto/keys/client/consts.go b/pkgs/crypto/keys/client/consts.go deleted file mode 100644 index 3adf245b2c4..00000000000 --- a/pkgs/crypto/keys/client/consts.go +++ /dev/null @@ -1,5 +0,0 @@ -package client - -const ( - mnemonicEntropySize = 256 -) diff --git a/pkgs/crypto/keys/client/consts_test.go b/pkgs/crypto/keys/client/consts_test.go deleted file mode 100644 index de742d8e736..00000000000 --- a/pkgs/crypto/keys/client/consts_test.go +++ /dev/null @@ -1,6 +0,0 @@ -package client - -// consts for testing -const ( - testMnemonic = "equip will roof matter pink blind book anxiety banner elbow sun young" -) diff --git a/pkgs/crypto/keys/client/delete.go b/pkgs/crypto/keys/client/delete.go index 41a1f012a43..163fd36dee4 100644 --- a/pkgs/crypto/keys/client/delete.go +++ b/pkgs/crypto/keys/client/delete.go @@ -1,32 +1,63 @@ package client import ( - "github.com/gnolang/gno/pkgs/command" + "context" + "errors" + "flag" + + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" - "github.com/gnolang/gno/pkgs/errors" ) -type DeleteOptions struct { - BaseOptions - Yes bool `flag:"yes" help:"skip confirmation prompt"` - Force bool `flag:"force" help:"remove key unconditionally"` +type deleteCfg struct { + rootCfg *baseCfg + + yes bool + force bool } -var DefaultDeleteOptions = DeleteOptions{ - BaseOptions: DefaultBaseOptions, +func newDeleteCmd(rootCfg *baseCfg) *commands.Command { + cfg := &deleteCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "delete", + ShortUsage: "delete [flags] ", + ShortHelp: "Deletes a key from the keybase", + }, + cfg, + func(_ context.Context, args []string) error { + return execDelete(cfg, args, commands.NewDefaultIO()) + }, + ) } -func deleteApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(DeleteOptions) +func (c *deleteCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.yes, + "yes", + false, + "skip confirmation prompt", + ) + + fs.BoolVar( + &c.force, + "force", + false, + "remove key unconditionally", + ) +} +func execDelete(cfg *deleteCfg, args []string, io *commands.IO) error { if len(args) != 1 { - cmd.ErrPrintfln("Usage: delete ") - return errors.New("invalid args") + return flag.ErrHelp } nameOrBech32 := args[0] - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.Home) if err != nil { return err } @@ -37,24 +68,26 @@ func deleteApp(cmd *command.Command, args []string, iopts interface{}) error { } if info.GetType() == keys.TypeLedger || info.GetType() == keys.TypeOffline { - if !opts.Yes { - if err := confirmDeletion(cmd); err != nil { + if !cfg.yes { + if err := confirmDeletion(io); err != nil { return err } } + if err := kb.Delete(nameOrBech32, "", true); err != nil { return err } - cmd.ErrPrintln("Public key reference deleted") + io.ErrPrintln("Public key reference deleted") + return nil } // skip passphrase check if run with --force - skipPass := opts.Force + skipPass := cfg.force var oldpass string if !skipPass { msg := "DANGER - enter password to permanently delete key:" - if oldpass, err = cmd.GetPassword(msg, false); err != nil { + if oldpass, err = io.GetPassword(msg, cfg.rootCfg.InsecurePasswordStdin); err != nil { return err } } @@ -63,17 +96,21 @@ func deleteApp(cmd *command.Command, args []string, iopts interface{}) error { if err != nil { return err } - cmd.ErrPrintln("Key deleted") + io.ErrPrintln("Key deleted") + return nil } -func confirmDeletion(cmd *command.Command) error { - answer, err := cmd.GetConfirmation("Key reference will be deleted. Continue?") +func confirmDeletion(io *commands.IO) error { + answer, err := io.GetConfirmation("Key reference will be deleted. Continue?") + if err != nil { return err } + if !answer { return errors.New("aborted") } + return nil } diff --git a/pkgs/crypto/keys/client/delete_test.go b/pkgs/crypto/keys/client/delete_test.go index 9622114024a..8b3a8a78f10 100644 --- a/pkgs/crypto/keys/client/delete_test.go +++ b/pkgs/crypto/keys/client/delete_test.go @@ -4,48 +4,59 @@ import ( "strings" "testing" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" "github.com/gnolang/gno/pkgs/testutils" - "github.com/jaekwon/testify/assert" - "github.com/jaekwon/testify/require" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func Test_deleteApp(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) +const ( + testMnemonic = "equip will roof matter pink blind book anxiety banner elbow sun young" +) + +func Test_execDelete(t *testing.T) { + t.Parallel() // make new test dir kbHome, kbCleanUp := testutils.NewTestCaseDir(t) defer kbCleanUp() // initialize test options - opts := DeleteOptions{ - BaseOptions: BaseOptions{ - Home: kbHome, + cfg := &deleteCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + Home: kbHome, + InsecurePasswordStdin: true, + }, }, } + io := commands.NewTestIO() + fakeKeyName1 := "deleteApp_Key1" fakeKeyName2 := "deleteApp_Key2" // Add test accounts to keybase. - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(kbHome) assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName1, testMnemonic, "", "", 0, 0) assert.NoError(t, err) + _, err = kb.CreateAccount(fakeKeyName2, testMnemonic, "", "", 0, 1) assert.NoError(t, err) // test: Key not found args := []string{"blah"} - err = deleteApp(cmd, args, opts) + err = execDelete(cfg, args, nil) require.Error(t, err) require.Equal(t, err.Error(), "Key blah not found") // test: User confirmation missing args = []string{fakeKeyName1} - err = deleteApp(cmd, args, opts) + io.SetIn(strings.NewReader("")) + err = execDelete(cfg, args, io) require.Error(t, err) require.Equal(t, err.Error(), "EOF") @@ -54,30 +65,33 @@ func Test_deleteApp(t *testing.T) { require.NoError(t, err) // Now there is a blank password followed by a confirmation. - cmd.SetIn(strings.NewReader("\ny\n")) args := []string{fakeKeyName1} - err = deleteApp(cmd, args, opts) + io.SetIn(strings.NewReader("\ny\n")) + err = execDelete(cfg, args, io) require.NoError(t, err) _, err = kb.GetByName(fakeKeyName1) require.Error(t, err) // Key1 is gone } - // Set DeleteOptions.Yes = true - opts = DeleteOptions{ - BaseOptions: BaseOptions{ - Home: kbHome, + // Set config yes = true + cfg = &deleteCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + Home: kbHome, + InsecurePasswordStdin: true, + }, }, - Yes: true, + yes: true, } _, err = kb.GetByName(fakeKeyName2) require.NoError(t, err) // Run again with blank password followed by eof. - cmd.SetIn(strings.NewReader("\n")) args = []string{fakeKeyName2} - err = deleteApp(cmd, args, opts) + io.SetIn(strings.NewReader("\n")) + err = execDelete(cfg, args, io) require.NoError(t, err) _, err = kb.GetByName(fakeKeyName2) require.Error(t, err) // Key2 is gone diff --git a/pkgs/crypto/keys/client/export.go b/pkgs/crypto/keys/client/export.go index 040dc49aa08..785f31999fd 100644 --- a/pkgs/crypto/keys/client/export.go +++ b/pkgs/crypto/keys/client/export.go @@ -1,55 +1,79 @@ package client import ( - "errors" + "context" + "flag" "fmt" "os" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" ) -var errInvalidExportArgs = errors.New("invalid export arguments provided") +type exportCfg struct { + rootCfg *baseCfg -type ExportOptions struct { - BaseOptions - - // The name or address of the private key to be exported - NameOrBech32 string `flag:"key" help:"Name or Bech32 address of the private key"` + nameOrBech32 string + outputPath string + unsafe bool +} - // Output path for the private key armor - OutputPath string `flag:"output-path" help:"The desired output path for the armor file"` +func newExportCmd(rootCfg *baseCfg) *commands.Command { + cfg := &exportCfg{ + rootCfg: rootCfg, + } - // Unsafe flag for specifying the output as unencrypted - Unsafe bool `flag:"unsafe" help:"Export the private key armor as unencrypted"` + return commands.NewCommand( + commands.Metadata{ + Name: "export", + ShortUsage: "export [flags]", + ShortHelp: "Exports private key armor", + }, + cfg, + func(_ context.Context, args []string) error { + return execExport(cfg, commands.NewDefaultIO()) + }, + ) } -var DefaultExportOptions = ExportOptions{ - BaseOptions: DefaultBaseOptions, -} +func (c *exportCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.nameOrBech32, + "key", + "", + "Name or Bech32 address of the private key", + ) -// exportApp performs private key exports using the provided params -func exportApp(cmd *command.Command, _ []string, iopts interface{}) error { - // Read the flag values - opts, ok := iopts.(ExportOptions) - if !ok { - return errInvalidExportArgs - } + fs.StringVar( + &c.outputPath, + "output-path", + "", + "The desired output path for the armor file", + ) + fs.BoolVar( + &c.unsafe, + "unsafe", + false, + "Export the private key armor as unencrypted", + ) +} + +func execExport(cfg *exportCfg, io *commands.IO) error { // Create a new instance of the key-base - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.Home) if err != nil { return fmt.Errorf( "unable to create a key base from directory %s, %w", - opts.Home, + cfg.rootCfg.Home, err, ) } // Get the key-base decrypt password - decryptPassword, err := cmd.GetPassword( + decryptPassword, err := io.GetPassword( "Enter a passphrase to decrypt your private key from disk:", - false, + cfg.rootCfg.InsecurePasswordStdin, ) if err != nil { return fmt.Errorf( @@ -63,17 +87,21 @@ func exportApp(cmd *command.Command, _ []string, iopts interface{}) error { exportErr error ) - if opts.Unsafe { + if cfg.unsafe { // Generate the unencrypted armor armor, exportErr = kb.ExportPrivKeyUnsafe( - opts.NameOrBech32, + cfg.nameOrBech32, decryptPassword, ) } else { // Get the armor encrypt password - encryptPassword, err := cmd.GetCheckPassword( - "Enter a passphrase to encrypt your private key armor:", - "Repeat the passphrase:") + encryptPassword, err := io.GetCheckPassword( + [2]string{ + "Enter a passphrase to encrypt your private key armor:", + "Repeat the passphrase:", + }, + cfg.rootCfg.InsecurePasswordStdin, + ) if err != nil { return fmt.Errorf( "unable to retrieve armor encrypt password from user, %w", @@ -83,7 +111,7 @@ func exportApp(cmd *command.Command, _ []string, iopts interface{}) error { // Generate the encrypted armor armor, exportErr = kb.ExportPrivKey( - opts.NameOrBech32, + cfg.nameOrBech32, decryptPassword, encryptPassword, ) @@ -98,9 +126,9 @@ func exportApp(cmd *command.Command, _ []string, iopts interface{}) error { // Write the armor to disk if err := os.WriteFile( - opts.OutputPath, + cfg.outputPath, []byte(armor), - 0o644, + 0644, ); err != nil { return fmt.Errorf( "unable to write encrypted armor to file, %w", @@ -108,7 +136,7 @@ func exportApp(cmd *command.Command, _ []string, iopts interface{}) error { ) } - cmd.Printfln("Private key armor successfully outputted to %s", opts.OutputPath) + io.Printfln("Private key armor successfully outputted to %s", cfg.outputPath) return nil } diff --git a/pkgs/crypto/keys/client/export_test.go b/pkgs/crypto/keys/client/export_test.go index 3ea1d7c23e0..39cfebb7f45 100644 --- a/pkgs/crypto/keys/client/export_test.go +++ b/pkgs/crypto/keys/client/export_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" "github.com/gnolang/gno/pkgs/testutils" "github.com/stretchr/testify/assert" @@ -84,31 +84,23 @@ func exportKey( input io.Reader, ) error { var ( - cmd = command.NewMockCommand() - opts = ExportOptions{ - BaseOptions: BaseOptions{ - Home: exportOpts.kbHome, + cfg = &exportCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + Home: exportOpts.kbHome, + InsecurePasswordStdin: true, + }, }, - NameOrBech32: exportOpts.keyName, - OutputPath: exportOpts.outputPath, - Unsafe: exportOpts.unsafe, + nameOrBech32: exportOpts.keyName, + outputPath: exportOpts.outputPath, + unsafe: exportOpts.unsafe, } ) - cmd.SetIn( - strings.NewReader( - fmt.Sprintf( - "%s\n%s\n%s\n", - exportOpts.decryptPassword, - exportOpts.encryptPassword, - exportOpts.encryptPassword, - ), - ), - ) - - cmd.SetIn(input) + cmdIO := commands.NewTestIO() + cmdIO.SetIn(input) - return exportApp(cmd, nil, opts) + return execExport(cfg, cmdIO) } // TestExport_ExportKey makes sure the key can be exported correctly diff --git a/pkgs/crypto/keys/client/generate.go b/pkgs/crypto/keys/client/generate.go index e47a0f2802a..5014a78e948 100644 --- a/pkgs/crypto/keys/client/generate.go +++ b/pkgs/crypto/keys/client/generate.go @@ -1,43 +1,71 @@ package client import ( + "context" "crypto/sha256" + "flag" "fmt" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/bip39" - "github.com/gnolang/gno/pkgs/errors" ) -type GenerateOptions struct { - CustomEntropy bool `flag:"entropy" help:"custom entropy"` +type generateCfg struct { + rootCfg *baseCfg + + customEntropy bool +} + +func newGenerateCmd(rootCfg *baseCfg) *commands.Command { + cfg := &generateCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "generate", + ShortUsage: "generate [flags]", + ShortHelp: "Generates a bip39 mnemonic", + }, + cfg, + func(_ context.Context, args []string) error { + return execGenerate(cfg, args, commands.NewDefaultIO()) + }, + ) } -var DefaultGenerateOptions = GenerateOptions{ - // BaseOptions: DefaultBaseOptions, +func (c *generateCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.customEntropy, + "entropy", + false, + "supply custom entropy", + ) } -func generateApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(GenerateOptions) - customEntropy := opts.CustomEntropy +func execGenerate(cfg *generateCfg, args []string, io *commands.IO) error { + customEntropy := cfg.customEntropy if len(args) != 0 { - cmd.ErrPrintfln("Usage: generate (no args)") - return errors.New("invalid args") + return flag.ErrHelp } var entropySeed []byte if customEntropy { // prompt the user to enter some entropy - inputEntropy, err := cmd.GetString("WARNING: Generate at least 256-bits of entropy and enter the results here:") + inputEntropy, err := io.GetString( + "WARNING: Generate at least 256-bits of entropy and enter the results here:", + ) if err != nil { return err } if len(inputEntropy) < 43 { return fmt.Errorf("256-bits is 43 characters in Base-64, and 100 in Base-6. You entered %v, and probably want more", len(inputEntropy)) } - conf, err := cmd.GetConfirmation(fmt.Sprintf("Input length: %d", len(inputEntropy))) + conf, err := io.GetConfirmation( + fmt.Sprintf("Input length: %d", len(inputEntropy)), + ) if err != nil { return err } @@ -61,7 +89,8 @@ func generateApp(cmd *command.Command, args []string, iopts interface{}) error { if err != nil { return err } - cmd.Println(mnemonic) + + io.Println(mnemonic) return nil } diff --git a/pkgs/crypto/keys/client/generate_test.go b/pkgs/crypto/keys/client/generate_test.go index 85cec3ac8c0..8a4b5c231c4 100644 --- a/pkgs/crypto/keys/client/generate_test.go +++ b/pkgs/crypto/keys/client/generate_test.go @@ -4,57 +4,57 @@ import ( "strings" "testing" - "github.com/gnolang/gno/pkgs/command" - "github.com/jaekwon/testify/assert" - "github.com/jaekwon/testify/require" + "github.com/gnolang/gno/pkgs/commands" + "github.com/stretchr/testify/require" ) -func Test_RunGenerateCmdNormal(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) +func Test_execGenerateNormal(t *testing.T) { + t.Parallel() - args := []string{} - opts := GenerateOptions{ - CustomEntropy: false, + cfg := &generateCfg{ + customEntropy: false, } - err := generateApp(cmd, args, opts) + + err := execGenerate(cfg, []string{}, commands.NewTestIO()) require.NoError(t, err) } -func Test_RunGenerateCmdUser(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) +func Test_execGenerateUser(t *testing.T) { + t.Parallel() - args := []string{} - opts := GenerateOptions{ - CustomEntropy: true, + cfg := &generateCfg{ + customEntropy: true, } - err := generateApp(cmd, args, opts) + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("")) + + err := execGenerate(cfg, []string{}, io) require.Error(t, err) require.Equal(t, err.Error(), "EOF") // Try again - cmd.SetIn(strings.NewReader("Hi!\n")) - err = generateApp(cmd, args, opts) + io.SetIn(strings.NewReader("Hi!\n")) + err = execGenerate(cfg, []string{}, io) require.Error(t, err) require.Equal(t, err.Error(), "256-bits is 43 characters in Base-64, and 100 in Base-6. You entered 3, and probably want more") // Now provide "good" entropy :) fakeEntropy := strings.Repeat(":)", 40) + "\ny\n" // entropy + accept count - cmd.SetIn(strings.NewReader(fakeEntropy)) - err = generateApp(cmd, args, opts) + io.SetIn(strings.NewReader(fakeEntropy)) + err = execGenerate(cfg, []string{}, io) require.NoError(t, err) // Now provide "good" entropy but no answer fakeEntropy = strings.Repeat(":)", 40) + "\n" // entropy + accept count - cmd.SetIn(strings.NewReader(fakeEntropy)) - err = generateApp(cmd, args, opts) + io.SetIn(strings.NewReader(fakeEntropy)) + err = execGenerate(cfg, []string{}, io) require.Error(t, err) // Now provide "good" entropy but say no fakeEntropy = strings.Repeat(":)", 40) + "\nn\n" // entropy + accept count - cmd.SetIn(strings.NewReader(fakeEntropy)) - err = generateApp(cmd, args, opts) + io.SetIn(strings.NewReader(fakeEntropy)) + err = execGenerate(cfg, []string{}, io) require.NoError(t, err) } diff --git a/pkgs/crypto/keys/client/import.go b/pkgs/crypto/keys/client/import.go index 41a0a3b3e17..33434ee0b5f 100644 --- a/pkgs/crypto/keys/client/import.go +++ b/pkgs/crypto/keys/client/import.go @@ -1,57 +1,81 @@ package client import ( - "errors" + "context" + "flag" "fmt" "os" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" ) -var errInvalidImportArgs = errors.New("invalid import arguments provided") +type importCfg struct { + rootCfg *baseCfg -type ImportOptions struct { - BaseOptions - - // Name of the private key in the key-base - KeyName string `flag:"name" help:"The name of the private key"` + keyName string + armorPath string + unsafe bool +} - // Path to the encrypted private key armor - ArmorPath string `flag:"armor-path" help:"The path to the encrypted armor file"` +func newImportCmd(rootCfg *baseCfg) *commands.Command { + cfg := &importCfg{ + rootCfg: rootCfg, + } - // Unsafe flag for specifying the input as unencrypted - Unsafe bool `flag:"unsafe" help:"Import the private key armor as unencrypted"` + return commands.NewCommand( + commands.Metadata{ + Name: "import", + ShortUsage: "import [flags]", + ShortHelp: "Imports encrypted private key armor", + }, + cfg, + func(_ context.Context, _ []string) error { + return execImport(cfg, commands.NewDefaultIO()) + }, + ) } -var DefaultImportOptions = ImportOptions{ - BaseOptions: DefaultBaseOptions, -} +func (c *importCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.keyName, + "name", + "", + "The name of the private key", + ) -// importApp performs private key imports using the provided params -func importApp(cmd *command.Command, _ []string, iopts interface{}) error { - // Read the flag values - opts, ok := iopts.(ImportOptions) - if !ok { - return errInvalidImportArgs - } + fs.StringVar( + &c.armorPath, + "armor-path", + "", + "The path to the encrypted armor file", + ) + + fs.BoolVar( + &c.unsafe, + "unsafe", + false, + "Import the private key armor as unencrypted", + ) +} +func execImport(cfg *importCfg, io *commands.IO) error { // Create a new instance of the key-base - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.Home) if err != nil { return fmt.Errorf( "unable to create a key base from directory %s, %w", - opts.Home, + cfg.rootCfg.Home, err, ) } // Read the raw encrypted armor - armor, err := os.ReadFile(opts.ArmorPath) + armor, err := os.ReadFile(cfg.armorPath) if err != nil { return fmt.Errorf( "unable to read armor from path %s, %w", - opts.ArmorPath, + cfg.armorPath, err, ) } @@ -61,11 +85,11 @@ func importApp(cmd *command.Command, _ []string, iopts interface{}) error { encryptPassword string ) - if !opts.Unsafe { + if !cfg.unsafe { // Get the armor decrypt password - decryptPassword, err = cmd.GetPassword( + decryptPassword, err = io.GetPassword( "Enter a passphrase to decrypt your private key armor:", - false, + cfg.rootCfg.InsecurePasswordStdin, ) if err != nil { return fmt.Errorf( @@ -76,9 +100,13 @@ func importApp(cmd *command.Command, _ []string, iopts interface{}) error { } // Get the key-base encrypt password - encryptPassword, err = cmd.GetCheckPassword( - "Enter a passphrase to encrypt your private key:", - "Repeat the passphrase:") + encryptPassword, err = io.GetCheckPassword( + [2]string{ + "Enter a passphrase to encrypt your private key:", + "Repeat the passphrase:", + }, + cfg.rootCfg.InsecurePasswordStdin, + ) if err != nil { return fmt.Errorf( "unable to retrieve key encrypt password from user, %w", @@ -86,10 +114,10 @@ func importApp(cmd *command.Command, _ []string, iopts interface{}) error { ) } - if opts.Unsafe { + if cfg.unsafe { // Import the unencrypted private key if err := kb.ImportPrivKeyUnsafe( - opts.KeyName, + cfg.keyName, string(armor), encryptPassword, ); err != nil { @@ -101,7 +129,7 @@ func importApp(cmd *command.Command, _ []string, iopts interface{}) error { } else { // Import the encrypted private key if err := kb.ImportPrivKey( - opts.KeyName, + cfg.keyName, string(armor), decryptPassword, encryptPassword, @@ -113,7 +141,7 @@ func importApp(cmd *command.Command, _ []string, iopts interface{}) error { } } - cmd.Printfln("Successfully imported private key %s", opts.KeyName) + io.Printfln("Successfully imported private key %s", cfg.keyName) return nil } diff --git a/pkgs/crypto/keys/client/import_test.go b/pkgs/crypto/keys/client/import_test.go index 7d74ee6eb80..c891deb0d09 100644 --- a/pkgs/crypto/keys/client/import_test.go +++ b/pkgs/crypto/keys/client/import_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/testutils" "github.com/stretchr/testify/assert" ) @@ -23,20 +23,23 @@ func importKey( input io.Reader, ) error { var ( - cmd = command.NewMockCommand() - opts = ImportOptions{ - BaseOptions: BaseOptions{ - Home: importOpts.kbHome, + cfg = &importCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + Home: importOpts.kbHome, + InsecurePasswordStdin: true, + }, }, - KeyName: importOpts.keyName, - ArmorPath: importOpts.armorPath, - Unsafe: importOpts.unsafe, + keyName: importOpts.keyName, + armorPath: importOpts.armorPath, + unsafe: importOpts.unsafe, } ) - cmd.SetIn(input) + cmdIO := commands.NewTestIO() + cmdIO.SetIn(input) - return importApp(cmd, nil, opts) + return execImport(cfg, cmdIO) } // TestImport_ImportKey makes sure the key can be imported correctly diff --git a/pkgs/crypto/keys/client/list.go b/pkgs/crypto/keys/client/list.go index 180aef29c19..dfbadb35148 100644 --- a/pkgs/crypto/keys/client/list.go +++ b/pkgs/crypto/keys/client/list.go @@ -1,46 +1,53 @@ package client import ( - "github.com/gnolang/gno/pkgs/command" + "context" + "flag" + + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" - "github.com/gnolang/gno/pkgs/errors" ) -type ListOptions struct { - BaseOptions // home, ... -} - -var DefaultListOptions = ListOptions{ - BaseOptions: DefaultBaseOptions, +func newListCmd(rootCfg *baseCfg) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "list", + ShortUsage: "list [flags]", + ShortHelp: "Lists all keys in the keybase", + }, + nil, + func(_ context.Context, args []string) error { + return execList(rootCfg, args, commands.NewDefaultIO()) + }, + ) } -func listApp(cmd *command.Command, args []string, iopts interface{}) error { +func execList(cfg *baseCfg, args []string, io *commands.IO) error { if len(args) != 0 { - cmd.ErrPrintfln("Usage: list (no args)") - return errors.New("invalid args") + return flag.ErrHelp } - opts := iopts.(ListOptions) - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(cfg.Home) if err != nil { return err } infos, err := kb.List() if err == nil { - printInfos(cmd, infos) + printInfos(infos, io) } + return err } -func printInfos(cmd *command.Command, infos []keys.Info) { +func printInfos(infos []keys.Info, io *commands.IO) { for i, info := range infos { keyname := info.GetName() keytype := info.GetType() keypub := info.GetPubKey() keyaddr := info.GetAddress() keypath, _ := info.GetPath() - cmd.Printfln("%d. %s (%s) - addr: %v pub: %v, path: %v", + io.Printfln("%d. %s (%s) - addr: %v pub: %v, path: %v", i, keyname, keytype, keyaddr, keypub, keypath) } } diff --git a/pkgs/crypto/keys/client/list_test.go b/pkgs/crypto/keys/client/list_test.go index db26a7109f6..bcc148ad910 100644 --- a/pkgs/crypto/keys/client/list_test.go +++ b/pkgs/crypto/keys/client/list_test.go @@ -3,16 +3,13 @@ package client import ( "testing" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" "github.com/gnolang/gno/pkgs/testutils" - "github.com/jaekwon/testify/assert" + "github.com/stretchr/testify/assert" ) -func Test_listApp(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) - +func Test_execList(t *testing.T) { // Prepare some keybases kbHome1, cleanUp1 := testutils.NewTestCaseDir(t) kbHome2, cleanUp2 := testutils.NewTestCaseDir(t) @@ -39,13 +36,14 @@ func Test_listApp(t *testing.T) { for _, tt := range testData { t.Run(tt.name, func(t *testing.T) { // Set current home - opts := ListOptions{ + cfg := &baseCfg{ BaseOptions: BaseOptions{ Home: tt.kbDir, }, } + args := tt.args - if err := listApp(cmd, args, opts); (err != nil) != tt.wantErr { + if err := execList(cfg, args, commands.NewTestIO()); (err != nil) != tt.wantErr { t.Errorf("listApp() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/pkgs/crypto/keys/client/maketx.go b/pkgs/crypto/keys/client/maketx.go new file mode 100644 index 00000000000..95030f88964 --- /dev/null +++ b/pkgs/crypto/keys/client/maketx.go @@ -0,0 +1,79 @@ +package client + +import ( + "flag" + + "github.com/gnolang/gno/pkgs/commands" +) + +type makeTxCfg struct { + rootCfg *baseCfg + + gasWanted int64 + gasFee string + memo string + + broadcast bool + chainID string +} + +func newMakeTxCmd(rootCfg *baseCfg) *commands.Command { + cfg := &makeTxCfg{ + rootCfg: rootCfg, + } + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "maketx", + ShortUsage: " [flags] [...]", + ShortHelp: "Composes a tx document to sign", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newAddPkgCmd(cfg), + newSendCmd(cfg), + newCallCmd(cfg), + ) + + return cmd +} + +func (c *makeTxCfg) RegisterFlags(fs *flag.FlagSet) { + fs.Int64Var( + &c.gasWanted, + "gas-wanted", + 0, + "gas requested for tx", + ) + + fs.StringVar( + &c.gasFee, + "gas-fee", + "", + "gas payment fee", + ) + + fs.StringVar( + &c.memo, + "memo", + "", + "any descriptive text", + ) + + fs.BoolVar( + &c.broadcast, + "broadcast", + false, + "sign and broadcast", + ) + + fs.StringVar( + &c.chainID, + "chainid", + "dev", + "chainid to sign for (only useful if --broadcast)", + ) +} diff --git a/pkgs/crypto/keys/client/query.go b/pkgs/crypto/keys/client/query.go index c128dc98e96..8a6f5d6ee62 100644 --- a/pkgs/crypto/keys/client/query.go +++ b/pkgs/crypto/keys/client/query.go @@ -1,38 +1,76 @@ package client import ( + "context" + "flag" "fmt" "github.com/gnolang/gno/pkgs/bft/rpc/client" ctypes "github.com/gnolang/gno/pkgs/bft/rpc/core/types" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/errors" ) -type QueryOptions struct { - BaseOptions // home,remote,... - Data []byte `flag:"data" help:"query data bytes"` // \n for queryexprs. - Height int64 `flag:"height" help:"query height (not yet supported)"` // not yet used - Prove bool `flag:"prove" help:"prove query result (not yet supported)"` // not yet used +type queryCfg struct { + rootCfg *baseCfg + + data string + height int64 + prove bool // internal - Path string `flag:"-"` + path string } -var DefaultQueryOptions = QueryOptions{ - BaseOptions: DefaultBaseOptions, +func newQueryCmd(rootCfg *baseCfg) *commands.Command { + cfg := &queryCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "query", + ShortUsage: "query [flags] ", + ShortHelp: "Makes an ABCI query", + }, + nil, + func(_ context.Context, args []string) error { + return execQuery(cfg, args) + }, + ) } -func queryApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(QueryOptions) +func (c *queryCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.data, + "data", + "", + "query data bytes", + ) + + fs.Int64Var( + &c.height, + "height", + 0, + "query height (not yet supported)", + ) + fs.BoolVar( + &c.prove, + "prove", + false, + "prove query result (not yet supported)", + ) +} + +func execQuery(cfg *queryCfg, args []string) error { if len(args) != 1 { - cmd.ErrPrintfln("Usage: query ") - return errors.New("invalid args") + return flag.ErrHelp } - opts.Path = args[0] - qres, err := QueryHandler(opts) + cfg.path = args[0] + + qres, err := queryHandler(cfg) if err != nil { return err } @@ -52,20 +90,20 @@ func queryApp(cmd *command.Command, args []string, iopts interface{}) error { return nil } -func QueryHandler(opts QueryOptions) (*ctypes.ResultABCIQuery, error) { - remote := opts.Remote +func queryHandler(cfg *queryCfg) (*ctypes.ResultABCIQuery, error) { + remote := cfg.rootCfg.Remote if remote == "" || remote == "y" { return nil, errors.New("missing remote url") } - data := opts.Data + data := []byte(cfg.data) opts2 := client.ABCIQueryOptions{ // Height: height, XXX // Prove: false, XXX } cli := client.NewHTTP(remote, "/websocket") qres, err := cli.ABCIQueryWithOptions( - opts.Path, data, opts2) + cfg.path, data, opts2) if err != nil { return nil, errors.Wrap(err, "querying") } diff --git a/pkgs/crypto/keys/client/root.go b/pkgs/crypto/keys/client/root.go index 9f6713eb550..cb96c3009c9 100644 --- a/pkgs/crypto/keys/client/root.go +++ b/pkgs/crypto/keys/client/root.go @@ -1,56 +1,76 @@ +// Dedicated to my love, Lexi. package client import ( - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/errors" + "flag" + + "github.com/gnolang/gno/pkgs/commands" ) -type ( - AppItem = command.AppItem - AppList = command.AppList +const ( + mnemonicEntropySize = 256 ) -var mainApps AppList = []AppItem{ - {addApp, "add", "add key to keybase", DefaultAddOptions}, - {deleteApp, "delete", "delete key from keybase", DefaultDeleteOptions}, - {generateApp, "generate", "generate a new private key", DefaultGenerateOptions}, - {exportApp, "export", "export encrypted private key armor", DefaultExportOptions}, - {importApp, "import", "import encrypted private key armor", DefaultImportOptions}, - {listApp, "list", "list all known keys", DefaultListOptions}, - {signApp, "sign", "sign a document", DefaultSignOptions}, - {verifyApp, "verify", "verify a document signature", DefaultVerifyOptions}, - {broadcastApp, "broadcast", "broadcast a signed document", DefaultBroadcastOptions}, - {queryApp, "query", "make an ABCI query", DefaultQueryOptions}, +type baseCfg struct { + BaseOptions } -// For clients that want to extend the functionality of the base client. -func AddApp(app command.App, name string, desc string, defaults interface{}) { - mainApps = append(mainApps, AppItem{ - App: app, - Name: name, - Desc: desc, - Defaults: defaults, - }) +func NewRootCmd() *commands.Command { + cfg := &baseCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + LongHelp: "Manages private keys for the node", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newAddCmd(cfg), + newDeleteCmd(cfg), + newGenerateCmd(cfg), + newExportCmd(cfg), + newImportCmd(cfg), + newListCmd(cfg), + newSignCmd(cfg), + newVerifyCmd(cfg), + newQueryCmd(cfg), + newBroadcastCmd(cfg), + newMakeTxCmd(cfg), + ) + + return cmd } -func RunMain(cmd *command.Command, exec string, args []string) error { - // show help message. - if len(args) == 0 || args[0] == "help" || args[0] == "--help" { - cmd.Println("available subcommands:") - for _, appItem := range mainApps { - cmd.Printf(" %s - %s\n", appItem.Name, appItem.Desc) - } - return nil - } - - // switch on first argument. - for _, appItem := range mainApps { - if appItem.Name == args[0] { - err := cmd.Run(appItem.App, args[1:], appItem.Defaults) - return err // done - } - } - - // unknown app command! - return errors.New("unknown command " + args[0]) +func (c *baseCfg) RegisterFlags(fs *flag.FlagSet) { + // Base options + fs.StringVar( + &c.Home, + "home", + DefaultBaseOptions.Home, + "home directory", + ) + + fs.StringVar( + &c.Remote, + "remote", + DefaultBaseOptions.Remote, + "remote node URL", + ) + + fs.BoolVar( + &c.Quiet, + "quiet", + DefaultBaseOptions.Quiet, + "suppress output during execution", + ) + + fs.BoolVar( + &c.InsecurePasswordStdin, + "insecure-password-stdin", + DefaultBaseOptions.Quiet, + "WARNING! take password from stdin", + ) } diff --git a/pkgs/crypto/keys/client/send.go b/pkgs/crypto/keys/client/send.go new file mode 100644 index 00000000000..d9c4d2b52cf --- /dev/null +++ b/pkgs/crypto/keys/client/send.go @@ -0,0 +1,130 @@ +package client + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/pkgs/amino" + "github.com/gnolang/gno/pkgs/commands" + "github.com/gnolang/gno/pkgs/crypto" + "github.com/gnolang/gno/pkgs/crypto/keys" + "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/sdk/bank" + "github.com/gnolang/gno/pkgs/std" +) + +type sendCfg struct { + rootCfg *makeTxCfg + + send string + to string +} + +func newSendCmd(rootCfg *makeTxCfg) *commands.Command { + cfg := &sendCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "send", + ShortUsage: "send [flags] ", + ShortHelp: "Sends native currency", + }, + cfg, + func(_ context.Context, args []string) error { + return execSend(cfg, args, commands.NewDefaultIO()) + }, + ) +} + +func (c *sendCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.send, + "send", + "", + "send amount", + ) + + fs.StringVar( + &c.to, + "to", + "", + "destination address", + ) +} + +func execSend(cfg *sendCfg, args []string, io *commands.IO) error { + if len(args) != 1 { + return flag.ErrHelp + } + + if cfg.rootCfg.gasWanted == 0 { + return errors.New("gas-wanted not specified") + } + if cfg.rootCfg.gasFee == "" { + return errors.New("gas-fee not specified") + } + if cfg.send == "" { + return errors.New("send (amount) must be specified") + } + if cfg.to == "" { + return errors.New("to (destination address) must be specified") + } + + // read account pubkey. + nameOrBech32 := args[0] + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.rootCfg.Home) + if err != nil { + return err + } + info, err := kb.GetByNameOrAddress(nameOrBech32) + if err != nil { + return err + } + fromAddr := info.GetAddress() + // info.GetPubKey() + + // Parse to address. + toAddr, err := crypto.AddressFromBech32(cfg.to) + if err != nil { + return err + } + + // Parse send amount. + send, err := std.ParseCoins(cfg.send) + if err != nil { + return errors.Wrap(err, "parsing send coins") + } + + // parse gas wanted & fee. + gaswanted := cfg.rootCfg.gasWanted + gasfee, err := std.ParseCoin(cfg.rootCfg.gasFee) + if err != nil { + return errors.Wrap(err, "parsing gas fee coin") + } + + // construct msg & tx and marshal. + msg := bank.MsgSend{ + FromAddress: fromAddr, + ToAddress: toAddr, + Amount: send, + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: cfg.rootCfg.memo, + } + + if cfg.rootCfg.broadcast { + err := signAndBroadcast(cfg.rootCfg, args, tx, io) + if err != nil { + return err + } + } else { + fmt.Println(string(amino.MustMarshalJSON(tx))) + } + return nil +} diff --git a/pkgs/crypto/keys/client/sign.go b/pkgs/crypto/keys/client/sign.go index c686c043d22..00eaf04ff18 100644 --- a/pkgs/crypto/keys/client/sign.go +++ b/pkgs/crypto/keys/client/sign.go @@ -1,71 +1,130 @@ package client import ( + "context" + "errors" + "flag" "fmt" "os" "github.com/gnolang/gno/pkgs/amino" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" - "github.com/gnolang/gno/pkgs/errors" "github.com/gnolang/gno/pkgs/std" ) -type SignOptions struct { - BaseOptions // home,... - TxPath string `flag:"txpath" help:"path to file of tx to sign"` - ChainID string `flag:"chainid" help:"chainid to sign for"` - AccountNumber *uint64 `flag:"number" help:"account number to sign with (required)"` - Sequence *uint64 `flag:"sequence" help:"sequence to sign with (required)"` - ShowSignBytes bool `flag:"show-signbytes" help:"show sign bytes and quit"` +type signCfg struct { + rootCfg *baseCfg + + txPath string + chainID string + accountNumber uint64 + sequence uint64 + showSignBytes bool // internal flags, when called programmatically - NameOrBech32 string `flag:"-"` - TxJSON []byte `flag:"-"` - Pass string `flag:"-"` + nameOrBech32 string + txJSON []byte + pass string +} + +func newSignCmd(rootCfg *baseCfg) *commands.Command { + cfg := &signCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "sign", + ShortUsage: "sign [flags] ", + ShortHelp: "Signs the document", + }, + cfg, + func(_ context.Context, args []string) error { + return execSign(cfg, args, commands.NewDefaultIO()) + }, + ) } -var DefaultSignOptions = SignOptions{ - BaseOptions: DefaultBaseOptions, - TxPath: "-", // read from stdin. - ChainID: "dev", +func (c *signCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.txPath, + "txpath", + "-", + "path to file of tx to sign", + ) + + fs.StringVar( + &c.chainID, + "chainid", + "dev", + "chainid to sign for", + ) + + fs.Uint64Var( + &c.accountNumber, + "number", + 0, + "account number to sign with (required)", + ) + + fs.Uint64Var( + &c.sequence, + "sequence", + 0, + "sequence to sign with (required)", + ) + + fs.BoolVar( + &c.showSignBytes, + "show-signbytes", + false, + "show sign bytes and quit", + ) } -func signApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(SignOptions) +func execSign(cfg *signCfg, args []string, io *commands.IO) error { var err error if len(args) != 1 { - cmd.ErrPrintfln("Usage: sign ") - return errors.New("invalid args") + return flag.ErrHelp } - opts.NameOrBech32 = args[0] + + cfg.nameOrBech32 = args[0] // read tx to sign - txpath := opts.TxPath + txpath := cfg.txPath if txpath == "-" { // from stdin. - txjsonstr, err := cmd.GetString("Enter tx to sign, terminated by a newline.") + txjsonstr, err := io.GetString( + "Enter tx to sign, terminated by a newline.", + ) if err != nil { return err } - opts.TxJSON = []byte(txjsonstr) + cfg.txJSON = []byte(txjsonstr) } else { // from file - opts.TxJSON, err = os.ReadFile(txpath) + cfg.txJSON, err = os.ReadFile(txpath) if err != nil { return err } } - if opts.Quiet { - opts.Pass, err = cmd.GetPassword("", opts.InsecurePasswordStdin) + if cfg.rootCfg.Quiet { + cfg.pass, err = io.GetPassword( + "", + cfg.rootCfg.InsecurePasswordStdin, + ) } else { - opts.Pass, err = cmd.GetPassword("Enter password.", opts.InsecurePasswordStdin) + cfg.pass, err = io.GetPassword( + "Enter password.", + cfg.rootCfg.InsecurePasswordStdin, + ) } if err != nil { return err } - signedTx, err := SignHandler(opts) + signedTx, err := SignHandler(cfg) if err != nil { return err } @@ -74,31 +133,25 @@ func signApp(cmd *command.Command, args []string, iopts interface{}) error { if err != nil { return err } - cmd.Println(string(signedJSON)) + io.Println(string(signedJSON)) return nil } -func SignHandler(opts SignOptions) (*std.Tx, error) { +func SignHandler(cfg *signCfg) (*std.Tx, error) { var err error var tx std.Tx - if opts.AccountNumber == nil { - return nil, errors.New("invalid account number") - } - if opts.Sequence == nil { - return nil, errors.New("invalid sequence") - } - if opts.TxJSON == nil { + if cfg.txJSON == nil { return nil, errors.New("invalid tx content") } - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.Home) if err != nil { return nil, err } - err = amino.UnmarshalJSON(opts.TxJSON, &tx) + err = amino.UnmarshalJSON(cfg.txJSON, &tx) if err != nil { return nil, err } @@ -121,16 +174,16 @@ func SignHandler(opts SignOptions) (*std.Tx, error) { } // derive sign doc bytes. - chainID := opts.ChainID - accountNumber := *opts.AccountNumber - sequence := *opts.Sequence + chainID := cfg.chainID + accountNumber := cfg.accountNumber + sequence := cfg.sequence signbz := tx.GetSignBytes(chainID, accountNumber, sequence) - if opts.ShowSignBytes { + if cfg.showSignBytes { fmt.Printf("sign bytes: %X\n", signbz) return nil, nil } - sig, pub, err := kb.Sign(opts.NameOrBech32, opts.Pass, signbz) + sig, pub, err := kb.Sign(cfg.nameOrBech32, cfg.pass, signbz) if err != nil { return nil, err } @@ -147,8 +200,9 @@ func SignHandler(opts SignOptions) (*std.Tx, error) { } } if !found { - return nil, errors.New("addr %v (%s) not in signer set", - addr, opts.NameOrBech32) + return nil, errors.New( + fmt.Sprintf("addr %v (%s) not in signer set", addr, cfg.nameOrBech32), + ) } return &tx, nil diff --git a/pkgs/crypto/keys/client/sign_test.go b/pkgs/crypto/keys/client/sign_test.go index c7bf85438b4..adb303e3a6c 100644 --- a/pkgs/crypto/keys/client/sign_test.go +++ b/pkgs/crypto/keys/client/sign_test.go @@ -6,17 +6,16 @@ import ( "testing" "github.com/gnolang/gno/pkgs/amino" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" - testutils2 "github.com/gnolang/gno/pkgs/sdk/testutils" + sdkutils "github.com/gnolang/gno/pkgs/sdk/testutils" "github.com/gnolang/gno/pkgs/std" "github.com/gnolang/gno/pkgs/testutils" - "github.com/jaekwon/testify/assert" + "github.com/stretchr/testify/assert" ) -func Test_signAppBasic(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) +func Test_execSign(t *testing.T) { + t.Parallel() // make new test dir kbHome, kbCleanUp := testutils.NewTestCaseDir(t) @@ -24,60 +23,65 @@ func Test_signAppBasic(t *testing.T) { defer kbCleanUp() // initialize test options - opts := SignOptions{ - BaseOptions: BaseOptions{ - Home: kbHome, + cfg := &signCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + Home: kbHome, + InsecurePasswordStdin: true, + }, }, - TxPath: "-", // stdin - ChainID: "dev", - AccountNumber: new(uint64), - Sequence: new(uint64), + txPath: "-", // stdin + chainID: "dev", + accountNumber: 0, + sequence: 0, } fakeKeyName1 := "signApp_Key1" fakeKeyName2 := "signApp_Key2" encPassword := "12345678" + io := commands.NewTestIO() + // add test account to keybase. - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.Home) assert.NoError(t, err) acc, err := kb.CreateAccount(fakeKeyName1, testMnemonic, "", encPassword, 0, 0) addr := acc.GetAddress() assert.NoError(t, err) // create a tx to sign. - msg := testutils2.NewTestMsg(addr) + msg := sdkutils.NewTestMsg(addr) fee := std.NewFee(1, std.Coin{"ugnot", 1000000}) tx := std.NewTx([]std.Msg{msg}, fee, nil, "") txjson := string(amino.MustMarshalJSON(tx)) - cmd.SetIn(strings.NewReader(txjson)) args := []string{fakeKeyName1} - err = signApp(cmd, args, opts) + io.SetIn(strings.NewReader(txjson)) + err = execSign(cfg, args, io) assert.Error(t, err) - cmd.SetIn(strings.NewReader(txjson + "\n")) args = []string{fakeKeyName1} - err = signApp(cmd, args, opts) + io.SetIn(strings.NewReader(txjson + "\n")) + err = execSign(cfg, args, io) assert.Error(t, err) - cmd.SetIn(strings.NewReader( + args = []string{fakeKeyName2} + io.SetIn(strings.NewReader( fmt.Sprintf("%s\n%s\n", txjson, encPassword, ), )) - args = []string{fakeKeyName2} - err = signApp(cmd, args, opts) + err = execSign(cfg, args, io) assert.Error(t, err) - cmd.SetIn(strings.NewReader( + args = []string{fakeKeyName1} + io.SetIn(strings.NewReader( fmt.Sprintf("%s\n%s\n", txjson, encPassword, ), )) - args = []string{fakeKeyName1} - err = signApp(cmd, args, opts) + err = execSign(cfg, args, io) assert.NoError(t, err) } diff --git a/pkgs/crypto/keys/client/verify.go b/pkgs/crypto/keys/client/verify.go index be6e93f2bf2..acb06413289 100644 --- a/pkgs/crypto/keys/client/verify.go +++ b/pkgs/crypto/keys/client/verify.go @@ -1,32 +1,56 @@ package client import ( + "context" "encoding/hex" + "flag" "os" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" - "github.com/gnolang/gno/pkgs/errors" ) -type VerifyOptions struct { - BaseOptions - DocPath string `flag:"docpath" help:"path of document file to verify"` +type verifyCfg struct { + rootCfg *baseCfg + + docPath string +} + +func newVerifyCmd(rootCfg *baseCfg) *commands.Command { + cfg := &verifyCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "verify", + ShortUsage: "verify [flags] ", + ShortHelp: "Verifies the document signature", + }, + cfg, + func(_ context.Context, args []string) error { + return execVerify(cfg, args, commands.NewDefaultIO()) + }, + ) } -var DefaultVerifyOptions = VerifyOptions{ - BaseOptions: DefaultBaseOptions, - DocPath: "", // read from stdin. +func (c *verifyCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.docPath, + "docpath", + "", + "path of document file to verify", + ) } -func verifyApp(cmd *command.Command, args []string, iopts interface{}) error { - var kb keys.Keybase - var err error - opts := iopts.(VerifyOptions) +func execVerify(cfg *verifyCfg, args []string, io *commands.IO) error { + var ( + kb keys.Keybase + err error + ) if len(args) != 2 { - cmd.ErrPrintfln("Usage: verify ") - return errors.New("invalid args") + return flag.ErrHelp } name := args[0] @@ -34,8 +58,8 @@ func verifyApp(cmd *command.Command, args []string, iopts interface{}) error { if err != nil { return err } - docpath := opts.DocPath - kb, err = keys.NewKeyBaseFromDir(opts.Home) + docpath := cfg.docPath + kb, err = keys.NewKeyBaseFromDir(cfg.rootCfg.Home) if err != nil { return err } @@ -43,7 +67,9 @@ func verifyApp(cmd *command.Command, args []string, iopts interface{}) error { // read document to sign if docpath == "" { // from stdin. - msgstr, err := cmd.GetString("Enter document to sign.") + msgstr, err := io.GetString( + "Enter document to sign.", + ) if err != nil { return err } @@ -61,7 +87,7 @@ func verifyApp(cmd *command.Command, args []string, iopts interface{}) error { // verify signature. err = kb.Verify(name, msg, sig) if err == nil { - cmd.Println("Valid signature!") + io.Println("Valid signature!") } return err } diff --git a/pkgs/crypto/keys/client/verify_test.go b/pkgs/crypto/keys/client/verify_test.go index 89892dae3dc..1906c8894f4 100644 --- a/pkgs/crypto/keys/client/verify_test.go +++ b/pkgs/crypto/keys/client/verify_test.go @@ -6,15 +6,14 @@ import ( "strings" "testing" - "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/commands" "github.com/gnolang/gno/pkgs/crypto/keys" "github.com/gnolang/gno/pkgs/testutils" - "github.com/jaekwon/testify/assert" + "github.com/stretchr/testify/assert" ) -func Test_verifyAppBasic(t *testing.T) { - cmd := command.NewMockCommand() - assert.NotNil(t, cmd) +func Test_execVerify(t *testing.T) { + t.Parallel() // make new test dir kbHome, kbCleanUp := testutils.NewTestCaseDir(t) @@ -22,20 +21,25 @@ func Test_verifyAppBasic(t *testing.T) { defer kbCleanUp() // initialize test options - opts := VerifyOptions{ - BaseOptions: BaseOptions{ - Home: kbHome, + cfg := &verifyCfg{ + rootCfg: &baseCfg{ + BaseOptions: BaseOptions{ + Home: kbHome, + InsecurePasswordStdin: true, + }, }, - DocPath: "", + docPath: "", } + io := commands.NewTestIO() + fakeKeyName1 := "verifyApp_Key1" // encPassword := "12345678" encPassword := "" testMsg := "some message" // add test account to keybase. - kb, err := keys.NewKeyBaseFromDir(opts.Home) + kb, err := keys.NewKeyBaseFromDir(kbHome) assert.NoError(t, err) _, err = kb.CreateAccount(fakeKeyName1, testMnemonic, "", encPassword, 0, 0) assert.NoError(t, err) @@ -48,18 +52,24 @@ func Test_verifyAppBasic(t *testing.T) { testSigHex := hex.EncodeToString(testSig) // good signature passes test. - cmd.SetIn(strings.NewReader(fmt.Sprintf( - "%s\n", testMsg))) args := []string{fakeKeyName1, testSigHex} - err = verifyApp(cmd, args, opts) + io.SetIn( + strings.NewReader( + fmt.Sprintf("%s\n", testMsg), + ), + ) + err = execVerify(cfg, args, io) assert.NoError(t, err) // mutated bad signature fails test. testBadSig := testutils.MutateByteSlice(testSig) testBadSigHex := hex.EncodeToString(testBadSig) - cmd.SetIn(strings.NewReader(fmt.Sprintf( - "%s\n", testMsg))) args = []string{fakeKeyName1, testBadSigHex} - err = verifyApp(cmd, args, opts) + io.SetIn( + strings.NewReader( + fmt.Sprintf("%s\n", testMsg), + ), + ) + err = execVerify(cfg, args, io) assert.Error(t, err) }