diff --git a/.golangci.yml b/.golangci.yml index 3bedf5a..ad6f070 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,6 +16,8 @@ linters: # discontinued linters - deadcode - exhaustivestruct + - gochecknoglobals + - gochecknoinits - golint - ifshort - interfacer diff --git a/cmd/deposit.go b/cmd/deposit.go index 8a1042d..00734bd 100644 --- a/cmd/deposit.go +++ b/cmd/deposit.go @@ -14,7 +14,7 @@ var depositCmd = &cobra.Command{ If no proposal ID is given by the user, the latest proposal is queried and deposited for.`, Args: cobra.RangeArgs(0, 1), Run: func(_ *cobra.Command, args []string) { - bin, err := utils.NewEvmosTestingBinary() + bin, err := utils.NewBinary(collectConfig()) if err != nil { bin.Logger.Error().Msgf("error creating binary: %v", err) diff --git a/cmd/root.go b/cmd/root.go index 358cbbd..18d9dd4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,26 +3,93 @@ package cmd import ( "os" + "github.com/MalteHerrmann/evmos-utils/utils" + evmosutils "github.com/evmos/evmos/v17/utils" "github.com/spf13/cobra" ) -// rootCmd represents the base command when called without any subcommands. -// -//nolint:gochecknoglobals // required by cobra -var rootCmd = &cobra.Command{ - Use: "evmos-utils", - Short: "A collection of utilities to interact with an Evmos node during development.", - Long: `The evmos-utils collection offers helpers to interact with an Evmos node during development. +var ( + // rootCmd represents the base command when called without any subcommands. + // + //nolint:gochecknoglobals // required by cobra + rootCmd = &cobra.Command{ + Use: "evmos-utils", + Short: "A collection of utilities to interact with an Evmos node during development.", + Long: `The evmos-utils collection offers helpers to interact with an Evmos node during development. It can be used to test upgrades, deposit or vote for specific or the latest proposals, etc.`, -} + } + + // appd is the name of the binary to execute commands with. + appd string + // chainID is the chain ID of the network. + chainID string + // denom of the chain's fee token. + denom string + // home is the home directory of the binary. + home string + // keyringBackend is the keyring to use. + keyringBackend string + // node to post requests and transactions to. + node string +) //nolint:gochecknoinits // required by cobra func init() { - rootCmd.AddCommand(depositCmd) + rootCmd.PersistentFlags().StringVar( + &appd, + "bin", + "evmosd", + "Name of the binary to be executed", + ) + rootCmd.PersistentFlags().StringVar( + &chainID, + "chain-id", + evmosutils.TestnetChainID+"-1", + "Chain ID of the network", + ) + rootCmd.PersistentFlags().StringVar( + &denom, + "denom", + "aevmos", + "Fee token denomination of the network", + ) + rootCmd.PersistentFlags().StringVar( + &home, + "home", + ".tmp-evmosd", + "Home directory of the binary", + ) + rootCmd.PersistentFlags().StringVar( + &keyringBackend, + "keyring-backend", + "test", + "Keyring to use", + ) + rootCmd.PersistentFlags().StringVar( + &node, + "node", + "http://localhost:26657", + "Node to post queries and transactions to", + ) + rootCmd.AddCommand(upgradeCmd) + rootCmd.AddCommand(depositCmd) rootCmd.AddCommand(voteCmd) } +// collectConfig returns a BinaryConfig filled with the current configuration options +// that depend on the passed flags to the given CLI commands. +func collectConfig() utils.BinaryConfig { + return utils.BinaryConfig{ + Appd: appd, + ChainID: chainID, + Denom: denom, + Home: home, + KeyringBackend: keyringBackend, + Node: node, + } +} + // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { diff --git a/cmd/upgrade.go b/cmd/upgrade.go index f26613e..3296f57 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "regexp" "github.com/MalteHerrmann/evmos-utils/gov" @@ -16,28 +17,24 @@ var upgradeCmd = &cobra.Command{ Long: `Prepare an upgrade of a node by submitting a governance proposal, voting for it using all keys of in the keyring and having it pass.`, Args: cobra.ExactArgs(1), - Run: func(_ *cobra.Command, args []string) { - bin, err := utils.NewEvmosTestingBinary() + RunE: func(_ *cobra.Command, args []string) error { + bin, err := utils.NewBinary(collectConfig()) if err != nil { - bin.Logger.Error().Msgf("error creating binary: %v", err) - - return + return errors.Wrap(err, "error creating binary") } targetVersion := args[0] if matched, _ := regexp.MatchString(`v\d+\.\d+\.\d(-rc\d+)?`, targetVersion); !matched { - bin.Logger.Error().Msgf("invalid target version: %s; please use the format vX.Y.Z(-rc*).", targetVersion) - - return + return fmt.Errorf("invalid target version: %s; please use the format vX.Y.Z(-rc*)", targetVersion) } - if err := upgradeLocalNode(bin, targetVersion); err != nil { - bin.Logger.Error().Msgf("error upgrading local node: %v", err) - - return + if err = upgradeLocalNode(bin, targetVersion); err != nil { + return errors.Wrap(err, "error upgrading local node") } bin.Logger.Info().Msgf("successfully prepared upgrade to %s", targetVersion) + + return nil }, } diff --git a/cmd/vote.go b/cmd/vote.go index 14e4963..3485168 100644 --- a/cmd/vote.go +++ b/cmd/vote.go @@ -14,7 +14,7 @@ var voteCmd = &cobra.Command{ If no proposal ID is passed, the latest proposal on chain is queried and used.`, Args: cobra.RangeArgs(0, 1), Run: func(_ *cobra.Command, args []string) { - bin, err := utils.NewEvmosTestingBinary() + bin, err := utils.NewBinary(collectConfig()) if err != nil { bin.Logger.Error().Msgf("error creating binary: %v", err) diff --git a/gov/deposit.go b/gov/deposit.go index 523dba6..9b0e7b1 100644 --- a/gov/deposit.go +++ b/gov/deposit.go @@ -30,13 +30,12 @@ func Deposit(bin *utils.Binary, args []string) (int, error) { // DepositForProposal deposits the given amount for the proposal with the given proposalID // from the given account. func DepositForProposal(bin *utils.Binary, proposalID int, sender, deposit string) error { - _, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ + _, err := utils.ExecuteTx(bin, utils.TxArgs{ Subcommand: []string{ "tx", "gov", "deposit", strconv.Itoa(proposalID), deposit, }, - From: sender, - UseDefaults: true, - Quiet: true, + From: sender, + Quiet: true, }) if err != nil { return errors.Wrap(err, fmt.Sprintf("failed to deposit for proposal %d", proposalID)) @@ -48,7 +47,7 @@ func DepositForProposal(bin *utils.Binary, proposalID int, sender, deposit strin // GetMinDeposit returns the minimum deposit necessary for a proposal from the governance parameters of // the running chain. func GetMinDeposit(bin *utils.Binary) (sdk.Coins, error) { - out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ + out, err := utils.ExecuteQuery(bin, utils.QueryArgs{ Subcommand: []string{"q", "gov", "param", "deposit", "--output=json"}, Quiet: true, }) diff --git a/gov/proposal.go b/gov/proposal.go index e160d31..8814a52 100644 --- a/gov/proposal.go +++ b/gov/proposal.go @@ -12,13 +12,13 @@ import ( ) // buildUpgradeProposalCommand builds the command to submit a software upgrade proposal. -func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int) []string { +func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int, denom string) []string { return []string{ "tx", "gov", "submit-legacy-proposal", "software-upgrade", targetVersion, "--title", fmt.Sprintf("'Upgrade to %s'", targetVersion), "--description", fmt.Sprintf("'Upgrade to %s'", targetVersion), "--upgrade-height", strconv.Itoa(upgradeHeight), - "--deposit", "100000000000000000000aevmos", + "--deposit", "100000000000000000000" + denom, "--output", "json", "--no-validate", } @@ -49,7 +49,7 @@ func GetProposalIDFromSubmitEvents(events []sdk.StringEvent) (int, error) { // QueryLatestProposalID queries the latest proposal ID. func QueryLatestProposalID(bin *utils.Binary) (int, error) { - out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ + out, err := utils.ExecuteQuery(bin, utils.QueryArgs{ Subcommand: []string{"q", "gov", "proposals", "--output=json"}, Quiet: true, }) @@ -79,12 +79,11 @@ func QueryLatestProposalID(bin *utils.Binary) (int, error) { // SubmitUpgradeProposal submits a software upgrade proposal with the given target version and upgrade height. func SubmitUpgradeProposal(bin *utils.Binary, targetVersion string, upgradeHeight int) (int, error) { - upgradeProposal := buildUpgradeProposalCommand(targetVersion, upgradeHeight) + upgradeProposal := buildUpgradeProposalCommand(targetVersion, upgradeHeight, bin.Config.Denom) - out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ - Subcommand: upgradeProposal, - From: "dev0", - UseDefaults: true, + out, err := utils.ExecuteTx(bin, utils.TxArgs{ + Subcommand: upgradeProposal, + From: "dev0", }) if err != nil { return 0, errors.Wrap(err, diff --git a/gov/vote.go b/gov/vote.go index b86b4b0..d1df414 100644 --- a/gov/vote.go +++ b/gov/vote.go @@ -57,11 +57,10 @@ func SubmitAllVotesForProposal(bin *utils.Binary, proposalID int) error { // VoteForProposal votes for the proposal with the given ID using the given account. func VoteForProposal(bin *utils.Binary, proposalID int, sender string) (string, error) { - out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{ - Subcommand: []string{"tx", "gov", "vote", strconv.Itoa(proposalID), "yes"}, - From: sender, - UseDefaults: true, - Quiet: true, + out, err := utils.ExecuteTx(bin, utils.TxArgs{ + Subcommand: []string{"tx", "gov", "vote", strconv.Itoa(proposalID), "yes"}, + From: sender, + Quiet: true, }) if err != nil { return out, errors.Wrap(err, fmt.Sprintf("failed to vote for proposal %d", proposalID)) diff --git a/utils/binary.go b/utils/binary.go index 08ef0ea..9dd2bd8 100644 --- a/utils/binary.go +++ b/utils/binary.go @@ -4,7 +4,8 @@ import ( "fmt" "os" "os/exec" - "path" + "path/filepath" + "strings" "github.com/cosmos/cosmos-sdk/codec" "github.com/evmos/evmos/v17/app" @@ -17,29 +18,58 @@ import ( // Binary is a struct to hold the necessary information to execute commands // using a Cosmos SDK-based binary. type Binary struct { - // Cdc is the codec to be used for the client. - Cdc *codec.ProtoCodec - // Home is the home directory of the binary. - Home string - // Appd is the name of the binary to be executed, e.g. "evmosd". - Appd string // Accounts are the accounts stored in the local keyring. Accounts []Account + // Cdc is the codec to be used for the client. + Cdc *codec.ProtoCodec + + // Config is the configuration of the binary + Config BinaryConfig + // Logger is a logger to be used within all commands. Logger zerolog.Logger } +// BinaryConfig holds the configuration of the binary. +type BinaryConfig struct { + // Appd is the name of the binary to be executed, e.g. "evmosd". + Appd string + // ChainID is the chain ID of the network. + ChainID string + // Denom for the fee payments on transactions + Denom string + // Home is the home directory of the binary. + Home string + // KeyringBackend defines which keyring to use + KeyringBackend string + // Node is the endpoint for gRPC connections + Node string +} + // NewBinary returns a new Binary instance. -func NewBinary(home, appd string, logger zerolog.Logger) (*Binary, error) { - // check if home directory exists - if _, err := os.Stat(home); os.IsNotExist(err) { - return nil, errors.Wrap(err, "home directory does not exist: "+home) +func NewBinary(config BinaryConfig) (*Binary, error) { + userHome, err := os.UserHomeDir() + if err != nil { + return nil, errors.Wrap(err, "failed to get user home dir") } + // strip the home directory from the given home if already included + var homeDir string + if strings.Contains(config.Home, userHome) { + homeDir = config.Home + } else { + homeDir = filepath.Join(userHome, config.Home) + } + + if _, err = os.Stat(homeDir); os.IsNotExist(err) { + return nil, errors.Wrap(err, "home directory does not exist: "+homeDir) + } + + config.Home = homeDir + // check if binary is installed - _, err := exec.LookPath(appd) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("binary %q not installed", appd)) + if _, err = exec.LookPath(config.Appd); err != nil { + return nil, fmt.Errorf("binary %q not installed", config.Appd) } cdc, ok := GetCodec() @@ -47,36 +77,19 @@ func NewBinary(home, appd string, logger zerolog.Logger) (*Binary, error) { return nil, errors.Wrap(err, "failed to get codec") } - return &Binary{ - Cdc: cdc, - Home: home, - Appd: appd, - Logger: logger, - }, nil -} - -// NewEvmosTestingBinary returns a new Binary instance with the default home and appd -// setup for the Evmos local node testing setup. -func NewEvmosTestingBinary() (*Binary, error) { logger := log.Output(zerolog.ConsoleWriter{Out: os.Stdout}) - userHome, err := os.UserHomeDir() - if err != nil { - return &Binary{Logger: logger}, errors.Wrap(err, "failed to get user home dir") - } - - defaultEvmosdHome := path.Join(userHome, ".tmp-evmosd") - - bin, err := NewBinary(defaultEvmosdHome, "evmosd", logger) - if err != nil { - return nil, err + binary := &Binary{ + Cdc: cdc, + Config: config, + Logger: logger, } - if err = bin.GetAccounts(); err != nil { + if err = binary.getAccounts(); err != nil { return nil, err } - return bin, nil + return binary, nil } // GetCodec returns the codec to be used for the client. diff --git a/utils/constants.go b/utils/constants.go index 402d4ce..5b64417 100644 --- a/utils/constants.go +++ b/utils/constants.go @@ -1,14 +1,8 @@ package utils -import ( - evmosutils "github.com/evmos/evmos/v17/utils" -) - const ( // defaultFees is the amount of fees to be sent with a default transaction. - defaultFees int = 1e18 // 1 aevmos + defaultFees int = 1e16 // 0.01 evmos // DeltaHeight is the amount of blocks in the future that the upgrade will be scheduled. DeltaHeight = 20 - // denom is the denomination used for the local node. - denom = evmosutils.BaseDenom ) diff --git a/utils/keys.go b/utils/keys.go index 81793f5..2e80766 100644 --- a/utils/keys.go +++ b/utils/keys.go @@ -18,11 +18,11 @@ type Account struct { Delegations []stakingtypes.Delegation `json:"delegations"` } -// GetAccounts is a method to retrieve the binaries keys from the configured +// getAccounts is a method to retrieve the binaries keys from the configured // keyring backend and stores it in the Binary struct. -func (bin *Binary) GetAccounts() error { +func (bin *Binary) getAccounts() error { out, err := ExecuteBinaryCmd(bin, BinaryCmdArgs{ - Subcommand: []string{"keys", "list", "--output=json"}, + Subcommand: []string{"keys", "list", "--output=json", "--home", bin.Config.Home}, }) if err != nil { return err @@ -47,7 +47,7 @@ func FilterAccountsWithDelegations(bin *Binary) ([]Account, error) { } for _, acc := range bin.Accounts { - out, err := ExecuteBinaryCmd(bin, BinaryCmdArgs{ + out, err := ExecuteQuery(bin, QueryArgs{ Subcommand: []string{"query", "staking", "delegations", acc.Address, "--output=json"}, }) if err != nil { diff --git a/utils/utils.go b/utils/utils.go index b89cec1..8982be9 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -10,38 +10,66 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - evmosutils "github.com/evmos/evmos/v17/utils" ) +// QueryArgs are the arguments passed to a CLI query. +type QueryArgs struct { + Subcommand []string + Quiet bool +} + +// ExecuteQueryCmd executes a query command. +func ExecuteQuery(bin *Binary, args QueryArgs) (string, error) { + queryCommand := args.Subcommand + queryCommand = append(queryCommand, "--node", bin.Config.Node) + + return ExecuteBinaryCmd(bin, BinaryCmdArgs{ + Subcommand: queryCommand, + Quiet: args.Quiet, + }) +} + +// TxArgs are the arguments passed to a CLI transaction. +type TxArgs struct { + Subcommand []string + From string + Quiet bool +} + +// ExecuteTx executes a transaction using the given binary. +func ExecuteTx(bin *Binary, args TxArgs) (string, error) { + txCommand := args.Subcommand + txCommand = append(txCommand, + "--node", bin.Config.Node, + "--home", bin.Config.Home, + "--from", args.From, + "--keyring-backend", bin.Config.KeyringBackend, + "--gas", "auto", + "--fees", fmt.Sprintf("%d%s", defaultFees, bin.Config.Denom), + "--gas-adjustment", "1.3", + "-b", "sync", + "-y", + ) + + return ExecuteBinaryCmd(bin, BinaryCmdArgs{ + Subcommand: txCommand, + Quiet: args.Quiet, + }) +} + // BinaryCmdArgs are the arguments passed to be executed with the Evmos binary. type BinaryCmdArgs struct { - Subcommand []string - Home string - From string - UseDefaults bool - Quiet bool + Subcommand []string + Quiet bool } // ExecuteBinaryCmd executes a shell command and returns the output and error. func ExecuteBinaryCmd(bin *Binary, args BinaryCmdArgs) (string, error) { fullCommand := args.Subcommand - if args.Home == "" { - fullCommand = append(fullCommand, "--home", bin.Home) - } else { - fullCommand = append(fullCommand, "--home", args.Home) - } - - if args.From != "" { - fullCommand = append(fullCommand, "--from", args.From) - } - - if args.UseDefaults { - defaultFlags := getDefaultFlags() - fullCommand = append(fullCommand, defaultFlags...) - } + fmt.Println("Command: ", bin.Config.Appd, strings.Join(fullCommand, " ")) //#nosec G204 // no risk of injection here because only internal commands are passed - cmd := exec.Command(bin.Appd, fullCommand...) + cmd := exec.Command(bin.Config.Appd, fullCommand...) output, err := cmd.CombinedOutput() if err != nil && !args.Quiet { @@ -51,27 +79,10 @@ func ExecuteBinaryCmd(bin *Binary, args BinaryCmdArgs) (string, error) { return string(output), err } -// getDefaultFlags returns the default flags to be used for the Evmos binary. -func getDefaultFlags() []string { - chainID := evmosutils.TestnetChainID + "-1" - - defaultFlags := []string{ - "--chain-id", chainID, - "--keyring-backend", "test", - "--gas", "auto", - "--fees", fmt.Sprintf("%d%s", defaultFees, denom), - "--gas-adjustment", "1.3", - "-b", "sync", - "-y", - } - - return defaultFlags -} - // GetCurrentHeight returns the current block height of the node. func GetCurrentHeight(bin *Binary) (int, error) { - output, err := ExecuteBinaryCmd(bin, BinaryCmdArgs{ - Subcommand: []string{"q", "block", "--node", "http://localhost:26657"}, + output, err := ExecuteQuery(bin, QueryArgs{ + Subcommand: []string{"q", "block"}, }) if err != nil { return 0, fmt.Errorf("error executing command: %w", err) @@ -111,7 +122,7 @@ func GetTxEvents(bin *Binary, out string) ([]sdk.StringEvent, error) { ) for range nAttempts { - txOut, err = ExecuteBinaryCmd(bin, BinaryCmdArgs{ + txOut, err = ExecuteQuery(bin, QueryArgs{ Subcommand: []string{"q", "tx", txHash, "--output=json"}, Quiet: true, })