diff --git a/Makefile b/Makefile index 6c86730d0b2f..395af306ac35 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,8 @@ gaia: build: @rm -rf examples/basecoin/vendor/ - go build $(BUILD_FLAGS) -o build/basecoind ./examples/basecoin/cmd/basecoind/... + go build $(BUILD_FLAGS) -o build/basecoind ./examples/basecoin/cmd/basecoind + go build $(BUILD_FLAGS) -o build/basecli ./examples/basecoin/cmd/basecli dist: @bash publish/dist.sh diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index f1bd112b8434..c6ca46e0f830 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -24,7 +24,7 @@ var dbHeaderKey = []byte("header") // The ABCI application type BaseApp struct { // initialized on creation - logger log.Logger + Logger log.Logger name string // application name from abci.Info db dbm.DB // common DB backend cms sdk.CommitMultiStore // Main (uncached) state @@ -56,7 +56,7 @@ var _ abci.Application = (*BaseApp)(nil) // Create and name new BaseApp func NewBaseApp(name string, logger log.Logger, db dbm.DB) *BaseApp { return &BaseApp{ - logger: logger, + Logger: logger, name: name, db: db, cms: store.NewCommitMultiStore(db), @@ -361,12 +361,14 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk } // Run the ante handler. - newCtx, result, abort := app.anteHandler(ctx, tx) - if abort { - return result - } - if !newCtx.IsZero() { - ctx = newCtx + if app.anteHandler != nil { + newCtx, result, abort := app.anteHandler(ctx, tx) + if abort { + return result + } + if !newCtx.IsZero() { + ctx = newCtx + } } // Get the correct cache @@ -420,7 +422,7 @@ func (app *BaseApp) Commit() (res abci.ResponseCommit) { // Write the Deliver state and commit the MultiStore app.deliverState.ms.Write() commitID := app.cms.Commit() - app.logger.Debug("Commit synced", + app.Logger.Debug("Commit synced", "commit", commitID, ) diff --git a/client/flags.go b/client/flags.go new file mode 100644 index 000000000000..843cb52d1e93 --- /dev/null +++ b/client/flags.go @@ -0,0 +1,38 @@ +package client + +import "github.com/spf13/cobra" + +// nolint +const ( + FlagChainID = "chain-id" + FlagNode = "node" + FlagHeight = "height" + FlagTrustNode = "trust-node" + FlagName = "name" +) + +// LineBreak can be included in a command list to provide a blank line +// to help with readability +var LineBreak = &cobra.Command{Run: func(*cobra.Command, []string) {}} + +// GetCommands adds common flags to query commands +func GetCommands(cmds ...*cobra.Command) []*cobra.Command { + for _, c := range cmds { + // TODO: make this default false when we support proofs + c.Flags().Bool(FlagTrustNode, true, "Don't verify proofs for responses") + c.Flags().String(FlagChainID, "", "Chain ID of tendermint node") + c.Flags().String(FlagNode, "tcp://localhost:46657", ": to tendermint rpc interface for this chain") + c.Flags().Int64(FlagHeight, 0, "block height to query, omit to get most recent provable block") + } + return cmds +} + +// PostCommands adds common flags for commands to post tx +func PostCommands(cmds ...*cobra.Command) []*cobra.Command { + for _, c := range cmds { + c.Flags().String(FlagName, "", "Name of private key with which to sign") + c.Flags().String(FlagChainID, "", "Chain ID of tendermint node") + c.Flags().String(FlagNode, "tcp://localhost:46657", ": to tendermint rpc interface for this chain") + } + return cmds +} diff --git a/client/helpers.go b/client/helpers.go new file mode 100644 index 000000000000..10ffcc88e639 --- /dev/null +++ b/client/helpers.go @@ -0,0 +1,71 @@ +package client + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/viper" + + rpcclient "github.com/tendermint/tendermint/rpc/client" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + cmn "github.com/tendermint/tmlibs/common" +) + +// GetNode prepares a simple rpc.Client from the flags +func GetNode() (rpcclient.Client, error) { + uri := viper.GetString(FlagNode) + if uri == "" { + return nil, errors.New("Must define node using --node") + } + return rpcclient.NewHTTP(uri, "/websocket"), nil +} + +// Broadcast the transaction bytes to Tendermint +func BroadcastTx(tx []byte) (*ctypes.ResultBroadcastTxCommit, error) { + + node, err := GetNode() + if err != nil { + return nil, err + } + + res, err := node.BroadcastTxCommit(tx) + if err != nil { + return res, err + } + + if res.CheckTx.Code != uint32(0) { + return res, errors.Errorf("CheckTx failed: (%d) %s", + res.CheckTx.Code, + res.CheckTx.Log) + } + if res.DeliverTx.Code != uint32(0) { + return res, errors.Errorf("DeliverTx failed: (%d) %s", + res.DeliverTx.Code, + res.DeliverTx.Log) + } + return res, err +} + +// Query from Tendermint with the provided key and storename +func Query(key cmn.HexBytes, storeName string) (res []byte, err error) { + + path := fmt.Sprintf("/%s/key", storeName) + node, err := GetNode() + if err != nil { + return res, err + } + + opts := rpcclient.ABCIQueryOptions{ + Height: viper.GetInt64(FlagHeight), + Trusted: viper.GetBool(FlagTrustNode), + } + result, err := node.ABCIQueryWithOptions(path, key, opts) + if err != nil { + return res, err + } + resp := result.Response + if resp.Code != uint32(0) { + return res, errors.Errorf("Query failed: (%d) %s", resp.Code, resp.Log) + } + return resp.Value, nil +} diff --git a/client/input.go b/client/input.go new file mode 100644 index 000000000000..6036b68f9fe3 --- /dev/null +++ b/client/input.go @@ -0,0 +1,92 @@ +package client + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/bgentry/speakeasy" + isatty "github.com/mattn/go-isatty" + "github.com/pkg/errors" +) + +// MinPassLength is the minimum acceptable password length +const MinPassLength = 8 + +// BufferStdin is used to allow reading prompts for stdin +// multiple times, when we read from non-tty +func BufferStdin() *bufio.Reader { + return bufio.NewReader(os.Stdin) +} + +// GetPassword will prompt for a password one-time (to sign a tx) +// It enforces the password length +func GetPassword(prompt string, buf *bufio.Reader) (pass string, err error) { + if inputIsTty() { + pass, err = speakeasy.Ask(prompt) + } else { + pass, err = readLineFromBuf(buf) + } + if err != nil { + return "", err + } + if len(pass) < MinPassLength { + return "", errors.Errorf("Password must be at least %d characters", MinPassLength) + } + return pass, nil +} + +// GetSeed will request a seed phrase from stdin and trims off +// leading/trailing spaces +func GetSeed(prompt string, buf *bufio.Reader) (seed string, err error) { + if inputIsTty() { + fmt.Println(prompt) + } + seed, err = readLineFromBuf(buf) + seed = strings.TrimSpace(seed) + return +} + +// 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 GetCheckPassword(prompt, prompt2 string, buf *bufio.Reader) (string, error) { + // simple read on no-tty + if !inputIsTty() { + return GetPassword(prompt, buf) + } + + // TODO: own function??? + pass, err := GetPassword(prompt, buf) + if err != nil { + return "", err + } + pass2, err := GetPassword(prompt2, buf) + if err != nil { + return "", err + } + if pass != pass2 { + return "", errors.New("Passphrases don't match") + } + return pass, nil +} + +// inputIsTty returns true iff we have an interactive prompt, +// where we can disable echo and request to repeat the password. +// If false, we can optimize for piped input from another command +func inputIsTty() bool { + return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) +} + +// readLineFromBuf 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 readLineFromBuf(buf *bufio.Reader) (string, error) { + pass, err := buf.ReadString('\n') + if err != nil { + return "", err + } + return strings.TrimSpace(pass), nil +} diff --git a/client/keys.go b/client/keys.go new file mode 100644 index 000000000000..42eb00b7eb98 --- /dev/null +++ b/client/keys.go @@ -0,0 +1,23 @@ +package client + +import ( + "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-crypto/keys/words" + dbm "github.com/tendermint/tmlibs/db" +) + +// GetKeyBase initializes a keybase based on the configuration +func GetKeyBase(db dbm.DB) keys.Keybase { + keybase := keys.New( + db, + words.MustLoadCodec("english"), + ) + return keybase +} + +// MockKeyBase generates an in-memory keybase that will be discarded +// useful for --dry-run to generate a seed phrase without +// storing the key +func MockKeyBase() keys.Keybase { + return GetKeyBase(dbm.NewMemDB()) +} diff --git a/client/keys/README.md b/client/keys/README.md new file mode 100644 index 000000000000..029508ad5fd8 --- /dev/null +++ b/client/keys/README.md @@ -0,0 +1,119 @@ +# Keys CLI + +**WARNING: out-of-date and parts are wrong.... please update** + +This is as much an example how to expose cobra/viper, as for a cli itself +(I think this code is overkill for what go-keys needs). But please look at +the commands, and give feedback and changes. + +`RootCmd` calls some initialization functions (`cobra.OnInitialize` and `RootCmd.PersistentPreRunE`) which serve to connect environmental variables and cobra flags, as well as load the config file. It also validates the flags registered on root and creates the cryptomanager, which will be used by all subcommands. + +## Help info + +``` +# keys help + +Keys allows you to manage your local keystore for tendermint. + +These keys may be in any format supported by go-crypto and can be +used by light-clients, full nodes, or any other application that +needs to sign with a private key. + +Usage: + keys [command] + +Available Commands: + get Get details of one key + list List all keys + new Create a new public/private key pair + serve Run the key manager as an http server + update Change the password for a private key + +Flags: + --keydir string Directory to store private keys (subdir of root) (default "keys") + -o, --output string Output format (text|json) (default "text") + -r, --root string root directory for config and data (default "/Users/ethan/.tlc") + +Use "keys [command] --help" for more information about a command. +``` + +## Getting the config file + +The first step is to load in root, by checking the following in order: + +* -r, --root command line flag +* TM_ROOT environmental variable +* default ($HOME/.tlc evaluated at runtime) + +Once the `rootDir` is established, the script looks for a config file named `keys.{json,toml,yaml,hcl}` in that directory and parses it. These values will provide defaults for flags of the same name. + +There is an example config file for testing out locally, which writes keys to `./.mykeys`. You can + +## Getting/Setting variables + +When we want to get the value of a user-defined variable (eg. `output`), we can call `viper.GetString("output")`, which will do the following checks, until it finds a match: + +* Is `--output` command line flag present? +* Is `TM_OUTPUT` environmental variable set? +* Was a config file found and does it have an `output` variable? +* Is there a default set on the command line flag? + +If no variable is set and there was no default, we get back "". + +This setup allows us to have powerful command line flags, but use env variables or config files (local or 12-factor style) to avoid passing these arguments every time. + +## Nesting structures + +Sometimes we don't just need key-value pairs, but actually a multi-level config file, like + +``` +[mail] +from = "no-reply@example.com" +server = "mail.example.com" +port = 567 +password = "XXXXXX" +``` + +This CLI is too simple to warant such a structure, but I think eg. tendermint could benefit from such an approach. Here are some pointers: + +* [Accessing nested keys from config files](https://github.com/spf13/viper#accessing-nested-keys) +* [Overriding nested values with envvars](https://www.netlify.com/blog/2016/09/06/creating-a-microservice-boilerplate-in-go/#nested-config-values) - the mentioned outstanding PR is already merged into master! +* Overriding nested values with cli flags? (use `--log_config.level=info` ??) + +I'd love to see an example of this fully worked out in a more complex CLI. + +## Have your cake and eat it too + +It's easy to render data different ways. Some better for viewing, some better for importing to other programs. You can just add some global (persistent) flags to control the output formatting, and everyone gets what they want. + +``` +# keys list -e hex +All keys: +betty d0789984492b1674e276b590d56b7ae077f81adc +john b77f4720b220d1411a649b6c7f1151eb6b1c226a + +# keys list -e btc +All keys: +betty 3uTF4r29CbtnzsNHZoPSYsE4BDwH +john 3ZGp2Md35iw4XVtRvZDUaAEkCUZP + +# keys list -e b64 -o json +[ + { + "name": "betty", + "address": "0HiZhEkrFnTidrWQ1Wt64Hf4Gtw=", + "pubkey": { + "type": "secp256k1", + "data": "F83WvhT0KwttSoqQqd_0_r2ztUUaQix5EXdO8AZyREoV31Og780NW59HsqTAb2O4hZ-w-j0Z-4b2IjfdqqfhVQ==" + } + }, + { + "name": "john", + "address": "t39HILIg0UEaZJtsfxFR62scImo=", + "pubkey": { + "type": "ed25519", + "data": "t1LFmbg_8UTwj-n1wkqmnTp6NfaOivokEhlYySlGYCY=" + } + } +] +``` diff --git a/client/keys/add.go b/client/keys/add.go new file mode 100644 index 000000000000..7472dce27f9e --- /dev/null +++ b/client/keys/add.go @@ -0,0 +1,122 @@ +package keys + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/tmlibs/cli" +) + +const ( + flagType = "type" + flagRecover = "recover" + flagNoBackup = "no-backup" + flagDryRun = "dry-run" +) + +func addKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "add ", + Short: "Create a new key, or import from seed", + Long: `Add a public/private key pair to the key store. +If you select --seed/-s you can recover a key from the seed +phrase, otherwise, a new key will be generated.`, + RunE: runAddCmd, + } + cmd.Flags().StringP(flagType, "t", "ed25519", "Type of private key (ed25519|secp256k1|ledger)") + cmd.Flags().Bool(flagRecover, false, "Provide seed phrase to recover existing key instead of creating") + cmd.Flags().Bool(flagNoBackup, false, "Don't print out seed phrase (if others are watching the terminal)") + cmd.Flags().Bool(flagDryRun, false, "Perform action, but don't add key to local keystore") + return cmd +} + +func runAddCmd(cmd *cobra.Command, args []string) error { + var kb keys.Keybase + var err error + var name, pass string + + buf := client.BufferStdin() + if viper.GetBool(flagDryRun) { + // we throw this away, so don't enforce args, + // we want to get a new random seed phrase quickly + kb = client.MockKeyBase() + pass = "throwing-this-key-away" + name = "inmemorykey" + } else { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a name for the key") + } + name = args[0] + kb, err = GetKeyBase() + if err != nil { + return err + } + pass, err = client.GetCheckPassword( + "Enter a passphrase for your key:", + "Repeat the passphrase:", buf) + if err != nil { + return err + } + } + + if viper.GetBool(flagRecover) { + seed, err := client.GetSeed( + "Enter your recovery seed phrase:", buf) + if err != nil { + return err + } + info, err := kb.Recover(name, pass, seed) + if err != nil { + return err + } + // print out results without the seed phrase + viper.Set(flagNoBackup, true) + printCreate(info, "") + } else { + algo := keys.CryptoAlgo(viper.GetString(flagType)) + info, seed, err := kb.Create(name, pass, algo) + if err != nil { + return err + } + printCreate(info, seed) + } + return nil +} + +// addOutput lets us json format the data +type addOutput struct { + Key keys.Info `json:"key"` + Seed string `json:"seed"` +} + +func printCreate(info keys.Info, seed string) { + output := viper.Get(cli.OutputFlag) + switch output { + case "text": + printInfo(info) + // print seed unless requested not to. + if !viper.GetBool(flagNoBackup) { + fmt.Println("**Important** write this seed phrase in a safe place.") + fmt.Println("It is the only way to recover your account if you ever forget your password.") + fmt.Println() + fmt.Println(seed) + } + case "json": + out := addOutput{Key: info} + if !viper.GetBool(flagNoBackup) { + out.Seed = seed + } + json, err := MarshalJSON(out) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Println(string(json)) + default: + panic(fmt.Sprintf("I can't speak: %s", output)) + } +} diff --git a/client/keys/delete.go b/client/keys/delete.go new file mode 100644 index 000000000000..65a9513272ca --- /dev/null +++ b/client/keys/delete.go @@ -0,0 +1,45 @@ +package keys + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/pkg/errors" + + "github.com/spf13/cobra" +) + +func deleteKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete the given key", + RunE: runDeleteCmd, + } + return cmd +} + +func runDeleteCmd(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a name for the key") + } + name := args[0] + + buf := client.BufferStdin() + oldpass, err := client.GetPassword( + "DANGER - enter password to permanently delete key:", buf) + if err != nil { + return err + } + + kb, err := GetKeyBase() + if err != nil { + return err + } + + err = kb.Delete(name, oldpass) + if err != nil { + return err + } + fmt.Println("Password deleted forever (uh oh!)") + return nil +} diff --git a/client/keys/list.go b/client/keys/list.go new file mode 100644 index 000000000000..14c4408781d0 --- /dev/null +++ b/client/keys/list.go @@ -0,0 +1,25 @@ +package keys + +import "github.com/spf13/cobra" + +// listKeysCmd represents the list command +var listKeysCmd = &cobra.Command{ + Use: "list", + Short: "List all keys", + Long: `Return a list of all public keys stored by this key manager +along with their associated name and address.`, + RunE: runListCmd, +} + +func runListCmd(cmd *cobra.Command, args []string) error { + kb, err := GetKeyBase() + if err != nil { + return err + } + + infos, err := kb.List() + if err == nil { + printInfos(infos) + } + return err +} diff --git a/client/keys/root.go b/client/keys/root.go new file mode 100644 index 000000000000..962986c91a64 --- /dev/null +++ b/client/keys/root.go @@ -0,0 +1,29 @@ +package keys + +import ( + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" +) + +// Commands registers a sub-tree of commands to interact with +// local private key storage. +func Commands() *cobra.Command { + cmd := &cobra.Command{ + Use: "keys", + Short: "Add or view local private keys", + Long: `Keys allows you to manage your local keystore for tendermint. + + These keys may be in any format supported by go-crypto and can be + used by light-clients, full nodes, or any other application that + needs to sign with a private key.`, + } + cmd.AddCommand( + addKeyCommand(), + listKeysCmd, + showKeysCmd, + client.LineBreak, + deleteKeyCommand(), + updateKeyCommand(), + ) + return cmd +} diff --git a/client/keys/show.go b/client/keys/show.go new file mode 100644 index 000000000000..a22cb4bc887d --- /dev/null +++ b/client/keys/show.go @@ -0,0 +1,32 @@ +package keys + +import ( + "github.com/pkg/errors" + + "github.com/spf13/cobra" +) + +var showKeysCmd = &cobra.Command{ + Use: "show ", + Short: "Show key info for the given name", + Long: `Return public details of one local key.`, + RunE: runShowCmd, +} + +func runShowCmd(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a name for the key") + } + name := args[0] + + kb, err := GetKeyBase() + if err != nil { + return err + } + + info, err := kb.Get(name) + if err == nil { + printInfo(info) + } + return err +} diff --git a/client/keys/update.go b/client/keys/update.go new file mode 100644 index 000000000000..0e0f881c6e24 --- /dev/null +++ b/client/keys/update.go @@ -0,0 +1,50 @@ +package keys + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/pkg/errors" + + "github.com/spf13/cobra" +) + +func updateKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Change the password used to protect private key", + RunE: runUpdateCmd, + } + return cmd +} + +func runUpdateCmd(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a name for the key") + } + name := args[0] + + buf := client.BufferStdin() + oldpass, err := client.GetPassword( + "Enter the current passphrase:", buf) + if err != nil { + return err + } + newpass, err := client.GetCheckPassword( + "Enter the new passphrase:", + "Repeat the new passphrase:", buf) + if err != nil { + return err + } + + kb, err := GetKeyBase() + if err != nil { + return err + } + err = kb.Update(name, oldpass, newpass) + if err != nil { + return err + } + fmt.Println("Password successfully updated!") + return nil +} diff --git a/client/keys/utils.go b/client/keys/utils.go new file mode 100644 index 000000000000..19e63d74827e --- /dev/null +++ b/client/keys/utils.go @@ -0,0 +1,68 @@ +package keys + +import ( + "fmt" + + "github.com/spf13/viper" + + keys "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/tmlibs/cli" + dbm "github.com/tendermint/tmlibs/db" + + "github.com/cosmos/cosmos-sdk/client" +) + +// KeyDBName is the directory under root where we store the keys +const KeyDBName = "keys" + +var ( + // keybase is used to make GetKeyBase a singleton + keybase keys.Keybase +) + +// GetKeyBase initializes a keybase based on the configuration +func GetKeyBase() (keys.Keybase, error) { + if keybase == nil { + rootDir := viper.GetString(cli.HomeFlag) + db, err := dbm.NewGoLevelDB(KeyDBName, rootDir) + if err != nil { + return nil, err + } + keybase = client.GetKeyBase(db) + } + return keybase, nil +} + +func printInfo(info keys.Info) { + switch viper.Get(cli.OutputFlag) { + case "text": + addr := info.PubKey.Address().String() + sep := "\t\t" + if len(info.Name) > 7 { + sep = "\t" + } + fmt.Printf("%s%s%s\n", info.Name, sep, addr) + case "json": + json, err := MarshalJSON(info) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Println(string(json)) + } +} + +func printInfos(infos []keys.Info) { + switch viper.Get(cli.OutputFlag) { + case "text": + fmt.Println("All keys:") + for _, i := range infos { + printInfo(i) + } + case "json": + json, err := MarshalJSON(infos) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Println(string(json)) + } +} diff --git a/client/keys/wire.go b/client/keys/wire.go new file mode 100644 index 000000000000..5f7c15344e67 --- /dev/null +++ b/client/keys/wire.go @@ -0,0 +1,17 @@ +package keys + +import ( + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" +) + +var cdc *wire.Codec + +func init() { + cdc = wire.NewCodec() + crypto.RegisterWire(cdc) +} + +func MarshalJSON(o interface{}) ([]byte, error) { + return cdc.MarshalJSON(o) +} diff --git a/client/lcd/root.go b/client/lcd/root.go new file mode 100644 index 000000000000..9187af9a1a8d --- /dev/null +++ b/client/lcd/root.go @@ -0,0 +1,36 @@ +package lcd + +import ( + "errors" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" +) + +const ( + flagBind = "bind" + flagCORS = "cors" +) + +// XXX: remove this when not needed +func todoNotImplemented(_ *cobra.Command, _ []string) error { + return errors.New("TODO: Command not yet implemented") +} + +// ServeCommand will generate a long-running rest server +// (aka Light Client Daemon) that exposes functionality similar +// to the cli, but over rest +func ServeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Short: "Start LCD (light-client daemon), a local REST server", + RunE: todoNotImplemented, + } + // TODO: handle unix sockets also? + cmd.Flags().StringP(flagBind, "b", "localhost:1317", "Interface and port that server binds to") + cmd.Flags().String(flagCORS, "", "Set to domains that can make CORS requests (* for all)") + cmd.Flags().StringP(client.FlagChainID, "c", "", "ID of chain we connect to") + cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") + return cmd +} diff --git a/client/rpc/block.go b/client/rpc/block.go new file mode 100644 index 000000000000..c701061a2048 --- /dev/null +++ b/client/rpc/block.go @@ -0,0 +1,66 @@ +package rpc + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + tmwire "github.com/tendermint/tendermint/wire" +) + +const ( + flagSelect = "select" +) + +func blockCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "block [height]", + Short: "Get verified data for a the block at given height", + RunE: getBlock, + } + cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") + // TODO: change this to false when we can + cmd.Flags().Bool(client.FlagTrustNode, true, "Don't verify proofs for responses") + cmd.Flags().StringSlice(flagSelect, []string{"header", "tx"}, "Fields to return (header|txs|results)") + return cmd +} + +func getBlock(cmd *cobra.Command, args []string) error { + var height *int64 + // optional height + if len(args) > 0 { + h, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + if h > 0 { + tmp := int64(h) + height = &tmp + } + } + + // get the node + node, err := client.GetNode() + if err != nil { + return err + } + + // TODO: actually honor the --select flag! + // header -> BlockchainInfo + // header, tx -> Block + // results -> BlockResults + res, err := node.Block(height) + if err != nil { + return err + } + + output, err := tmwire.MarshalJSON(res) + // output, err := json.MarshalIndent(res, " ", "") + if err != nil { + return err + } + fmt.Println(string(output)) + return nil +} diff --git a/client/rpc/root.go b/client/rpc/root.go new file mode 100644 index 000000000000..8acf8ddad125 --- /dev/null +++ b/client/rpc/root.go @@ -0,0 +1,44 @@ +package rpc + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" +) + +const ( + // one of the following should be provided to verify the connection + flagGenesis = "genesis" + flagCommit = "commit" + flagValHash = "validator-set" +) + +// XXX: remove this when not needed +func todoNotImplemented(_ *cobra.Command, _ []string) error { + return errors.New("TODO: Command not yet implemented") +} + +// AddCommands adds a number of rpc-related subcommands +func AddCommands(cmd *cobra.Command) { + cmd.AddCommand( + initClientCommand(), + statusCommand(), + blockCommand(), + validatorCommand(), + ) +} + +func initClientCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize light client", + RunE: todoNotImplemented, + } + cmd.Flags().StringP(client.FlagChainID, "c", "", "ID of chain we connect to") + cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") + cmd.Flags().String(flagGenesis, "", "Genesis file to verify header validity") + cmd.Flags().String(flagCommit, "", "File with trusted and signed header") + cmd.Flags().String(flagValHash, "", "Hash of trusted validator set (hex-encoded)") + return cmd +} diff --git a/client/rpc/status.go b/client/rpc/status.go new file mode 100644 index 000000000000..c5888d99f5cc --- /dev/null +++ b/client/rpc/status.go @@ -0,0 +1,40 @@ +package rpc + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + tmwire "github.com/tendermint/tendermint/wire" +) + +func statusCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "Query remote node for status", + RunE: checkStatus, + } + cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") + return cmd +} + +func checkStatus(cmd *cobra.Command, args []string) error { + // get the node + node, err := client.GetNode() + if err != nil { + return err + } + res, err := node.Status() + if err != nil { + return err + } + + output, err := tmwire.MarshalJSON(res) + // output, err := json.MarshalIndent(res, " ", "") + if err != nil { + return err + } + fmt.Println(string(output)) + return nil +} diff --git a/client/rpc/validators.go b/client/rpc/validators.go new file mode 100644 index 000000000000..8eebda8dd690 --- /dev/null +++ b/client/rpc/validators.go @@ -0,0 +1,57 @@ +package rpc + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + tmwire "github.com/tendermint/tendermint/wire" +) + +func validatorCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "validatorset ", + Short: "Get the full validator set at given height", + RunE: getValidators, + } + cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") + // TODO: change this to false when we can + cmd.Flags().Bool(client.FlagTrustNode, true, "Don't verify proofs for responses") + return cmd +} + +func getValidators(cmd *cobra.Command, args []string) error { + var height *int64 + // optional height + if len(args) > 0 { + h, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + if h > 0 { + tmp := int64(h) + height = &tmp + } + } + + // get the node + node, err := client.GetNode() + if err != nil { + return err + } + + res, err := node.Validators(height) + if err != nil { + return err + } + + output, err := tmwire.MarshalJSON(res) + // output, err := json.MarshalIndent(res, " ", "") + if err != nil { + return err + } + fmt.Println(string(output)) + return nil +} diff --git a/client/tx/root.go b/client/tx/root.go new file mode 100644 index 000000000000..2099fb2112ac --- /dev/null +++ b/client/tx/root.go @@ -0,0 +1,20 @@ +package tx + +import ( + "github.com/spf13/cobra" + wire "github.com/tendermint/go-wire" +) + +// type used to pass around the provided cdc +type commander struct { + cdc *wire.Codec +} + +// AddCommands adds a number of tx-query related subcommands +func AddCommands(cmd *cobra.Command, cdc *wire.Codec) { + cmdr := commander{cdc} + cmd.AddCommand( + SearchTxCmd(cmdr), + QueryTxCmd(cmdr), + ) +} diff --git a/client/tx/search.go b/client/tx/search.go new file mode 100644 index 000000000000..ffe0ca323d94 --- /dev/null +++ b/client/tx/search.go @@ -0,0 +1,80 @@ +package tx + +import ( + "errors" + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + wire "github.com/tendermint/go-wire" + ctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +const ( + flagTags = "tag" + flagAny = "any" +) + +// default client command to search through tagged transactions +func SearchTxCmd(cmdr commander) *cobra.Command { + cmd := &cobra.Command{ + Use: "txs", + Short: "Search for all transactions that match the given tags", + RunE: cmdr.searchTxCmd, + } + cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") + // TODO: change this to false once proofs built in + cmd.Flags().Bool(client.FlagTrustNode, true, "Don't verify proofs for responses") + cmd.Flags().StringSlice(flagTags, nil, "Tags that must match (may provide multiple)") + cmd.Flags().Bool(flagAny, false, "Return transactions that match ANY tag, rather than ALL") + return cmd +} + +func (c commander) searchTxCmd(cmd *cobra.Command, args []string) error { + tags := viper.GetStringSlice(flagTags) + if len(tags) == 0 { + return errors.New("Must declare at least one tag to search") + } + // XXX: implement ANY + query := strings.Join(tags, " AND ") + + // get the node + node, err := client.GetNode() + if err != nil { + return err + } + + prove := !viper.GetBool(client.FlagTrustNode) + res, err := node.TxSearch(query, prove) + if err != nil { + return err + } + + info, err := formatTxResults(c.cdc, res) + if err != nil { + return err + } + + output, err := c.cdc.MarshalJSON(info) + if err != nil { + return err + } + fmt.Println(string(output)) + + return nil +} + +func formatTxResults(cdc *wire.Codec, res []*ctypes.ResultTx) ([]txInfo, error) { + var err error + out := make([]txInfo, len(res)) + for i := range res { + out[i], err = formatTxResult(cdc, res[i]) + if err != nil { + return nil, err + } + } + return out, nil +} diff --git a/client/tx/tx.go b/client/tx/tx.go new file mode 100644 index 000000000000..f9ac0631bfeb --- /dev/null +++ b/client/tx/tx.go @@ -0,0 +1,99 @@ +package tx + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/abci/types" + wire "github.com/tendermint/go-wire" + ctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// Get the default command for a tx query +func QueryTxCmd(cmdr commander) *cobra.Command { + cmd := &cobra.Command{ + Use: "tx [hash]", + Short: "Matches this txhash over all committed blocks", + RunE: cmdr.queryTxCmd, + } + cmd.Flags().StringP(client.FlagNode, "n", "tcp://localhost:46657", "Node to connect to") + // TODO: change this to false when we can + cmd.Flags().Bool(client.FlagTrustNode, true, "Don't verify proofs for responses") + return cmd +} + +// command to query for a transaction +func (c commander) queryTxCmd(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a tx hash") + } + + // find the key to look up the account + hexStr := args[0] + hash, err := hex.DecodeString(hexStr) + if err != nil { + return err + } + + // get the node + node, err := client.GetNode() + if err != nil { + return err + } + prove := !viper.GetBool(client.FlagTrustNode) + + res, err := node.Tx(hash, prove) + if err != nil { + return err + } + info, err := formatTxResult(c.cdc, res) + if err != nil { + return err + } + + output, err := json.MarshalIndent(info, "", " ") + if err != nil { + return err + } + fmt.Println(string(output)) + + return nil +} + +func formatTxResult(cdc *wire.Codec, res *ctypes.ResultTx) (txInfo, error) { + // TODO: verify the proof if requested + tx, err := parseTx(cdc, res.Tx) + if err != nil { + return txInfo{}, err + } + + info := txInfo{ + Height: res.Height, + Tx: tx, + Result: res.TxResult, + } + return info, nil +} + +// txInfo is used to prepare info to display +type txInfo struct { + Height int64 `json:"height"` + Tx sdk.Tx `json:"tx"` + Result abci.ResponseDeliverTx `json:"result"` +} + +func parseTx(cdc *wire.Codec, txBytes []byte) (sdk.Tx, error) { + var tx sdk.StdTx + err := cdc.UnmarshalBinary(txBytes, &tx) + if err != nil { + return nil, err + } + return tx, nil +} diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index b26a9bdd45c2..3c27faea92f8 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -40,7 +40,7 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { // create your application object var app = &BasecoinApp{ BaseApp: bam.NewBaseApp(appName, logger, db), - cdc: MakeTxCodec(), + cdc: MakeCodec(), capKeyMainStore: sdk.NewKVStoreKey("main"), capKeyIBCStore: sdk.NewKVStoreKey("ibc"), } @@ -71,10 +71,11 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { } // custom tx codec -func MakeTxCodec() *wire.Codec { +func MakeCodec() *wire.Codec { cdc := wire.NewCodec() - crypto.RegisterWire(cdc) // Register crypto.[PubKey,PrivKey,Signature] types. + cdc.RegisterInterface((*sdk.Msg)(nil), nil) bank.RegisterWire(cdc) // Register bank.[SendMsg,IssueMsg] types. + crypto.RegisterWire(cdc) // Register crypto.[PubKey,PrivKey,Signature] types. return cdc } diff --git a/examples/basecoin/app/app_test.go b/examples/basecoin/app/app_test.go index 20985fa32960..e917936b658c 100644 --- a/examples/basecoin/app/app_test.go +++ b/examples/basecoin/app/app_test.go @@ -53,14 +53,21 @@ func TestSendMsg(t *testing.T) { Signature: sig, }}) + // just marshal/unmarshal! + cdc := MakeCodec() + txBytes, err := cdc.MarshalBinary(tx) + require.NoError(t, err) + // Run a Check - res := bapp.Check(tx) - assert.Equal(t, sdk.CodeUnrecognizedAddress, res.Code, res.Log) + cres := bapp.CheckTx(txBytes) + assert.Equal(t, sdk.CodeUnrecognizedAddress, + sdk.CodeType(cres.Code), cres.Log) // Simulate a Block bapp.BeginBlock(abci.RequestBeginBlock{}) - res = bapp.Deliver(tx) - assert.Equal(t, sdk.CodeUnrecognizedAddress, res.Code, res.Log) + dres := bapp.DeliverTx(txBytes) + assert.Equal(t, sdk.CodeUnrecognizedAddress, + sdk.CodeType(dres.Code), dres.Log) } func TestGenesis(t *testing.T) { diff --git a/examples/basecoin/cmd/basecli/main.go b/examples/basecoin/cmd/basecli/main.go new file mode 100644 index 000000000000..638071d14f0e --- /dev/null +++ b/examples/basecoin/cmd/basecli/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "errors" + "os" + + "github.com/spf13/cobra" + + "github.com/tendermint/tmlibs/cli" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/lcd" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/version" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/commands" + bankcmd "github.com/cosmos/cosmos-sdk/x/bank/commands" + + "github.com/cosmos/cosmos-sdk/examples/basecoin/app" + "github.com/cosmos/cosmos-sdk/examples/basecoin/types" +) + +// gaiacliCmd is the entry point for this binary +var ( + basecliCmd = &cobra.Command{ + Use: "basecli", + Short: "Basecoin light-client", + } +) + +func todoNotImplemented(_ *cobra.Command, _ []string) error { + return errors.New("TODO: Command not yet implemented") +} + +func main() { + // disable sorting + cobra.EnableCommandSorting = false + + // get the codec + cdc := app.MakeCodec() + + // add standard rpc, and tx commands + rpc.AddCommands(basecliCmd) + basecliCmd.AddCommand(client.LineBreak) + tx.AddCommands(basecliCmd, cdc) + basecliCmd.AddCommand(client.LineBreak) + + // add query/post commands (custom to binary) + basecliCmd.AddCommand( + client.GetCommands( + authcmd.GetAccountCmd("main", cdc, types.GetParseAccount(cdc)), + )...) + basecliCmd.AddCommand( + client.PostCommands( + bankcmd.SendTxCmd(cdc), + )...) + + // add proxy, version and key info + basecliCmd.AddCommand( + client.LineBreak, + lcd.ServeCommand(), + keys.Commands(), + client.LineBreak, + version.VersionCmd, + ) + + // prepare and add flags + executor := cli.PrepareMainCmd(basecliCmd, "BC", os.ExpandEnv("$HOME/.basecli")) + executor.Execute() +} diff --git a/examples/basecoin/cmd/basecoind/main.go b/examples/basecoin/cmd/basecoind/main.go index f14867f407af..44ea00fbbab3 100644 --- a/examples/basecoin/cmd/basecoind/main.go +++ b/examples/basecoin/cmd/basecoind/main.go @@ -1,27 +1,76 @@ package main import ( + "encoding/json" "fmt" "os" + "github.com/spf13/cobra" + + abci "github.com/tendermint/abci/types" + "github.com/tendermint/tmlibs/cli" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" - "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/examples/basecoin/app" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/version" ) -func main() { - fmt.Println("This is temporary, for unblocking our build process.") - return +// basecoindCmd is the entry point for this binary +var ( + basecoindCmd = &cobra.Command{ + Use: "gaiad", + Short: "Gaia Daemon (server)", + } +) + +// defaultOptions sets up the app_options for the +// default genesis file +func defaultOptions(args []string) (json.RawMessage, error) { + addr, secret, err := server.GenerateCoinKey() + if err != nil { + return nil, err + } + fmt.Println("Secret phrase to access coins:") + fmt.Println(secret) - // TODO CREATE CLI - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "main") - db, err := dbm.NewGoLevelDB("basecoind", "data") + opts := fmt.Sprintf(`{ + "accounts": [{ + "address": "%s", + "coins": [ + { + "denom": "mycoin", + "amount": 9007199254740992 + } + ] + }] + }`, addr) + return json.RawMessage(opts), nil +} + +func generateApp(rootDir string, logger log.Logger) (abci.Application, error) { + db, err := dbm.NewGoLevelDB("basecoin", rootDir) if err != nil { - fmt.Println(err) - os.Exit(1) + return nil, err } bapp := app.NewBasecoinApp(logger, db) - baseapp.RunForever(bapp) + return bapp, nil +} + +func main() { + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). + With("module", "main") + + basecoindCmd.AddCommand( + server.InitCmd(defaultOptions, logger), + server.StartCmd(generateApp, logger), + server.UnsafeResetAllCmd(logger), + version.VersionCmd, + ) + + // prepare and add flags + rootDir := os.ExpandEnv("$HOME/.basecoind") + executor := cli.PrepareBaseCmd(basecoindCmd, "BC", rootDir) + executor.Execute() } diff --git a/examples/basecoin/types/account.go b/examples/basecoin/types/account.go index 3177f991220b..f5b4f7aa012e 100644 --- a/examples/basecoin/types/account.go +++ b/examples/basecoin/types/account.go @@ -4,6 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" ) var _ sdk.Account = (*AppAccount)(nil) @@ -15,13 +16,22 @@ var _ sdk.Account = (*AppAccount)(nil) // auth.AccountStore uses the flexible go-wire library. type AppAccount struct { auth.BaseAccount - Name string + Name string `json:"name"` } // nolint func (acc AppAccount) GetName() string { return acc.Name } func (acc *AppAccount) SetName(name string) { acc.Name = name } +// Get the ParseAccount function for the custom AppAccount +func GetParseAccount(cdc *wire.Codec) sdk.ParseAccount { + return func(accBytes []byte) (res sdk.Account, err error) { + acct := new(AppAccount) + err = cdc.UnmarshalBinary(accBytes, acct) + return acct, err + } +} + //___________________________________________________________________________________ // State to Unmarshal diff --git a/examples/gaia/gaiacli/client.go b/examples/gaia/gaiacli/client.go deleted file mode 100644 index 682a571e6ee9..000000000000 --- a/examples/gaia/gaiacli/client.go +++ /dev/null @@ -1,131 +0,0 @@ -package main - -import "github.com/spf13/cobra" - -const ( - // these are needed for every init - flagChainID = "chain-id" - flagNode = "node" - - // one of the following should be provided to verify the connection - flagGenesis = "genesis" - flagCommit = "commit" - flagValHash = "validator-set" - - flagSelect = "select" - flagTags = "tag" - flagAny = "any" - - flagBind = "bind" - flagCORS = "cors" - flagTrustNode = "trust-node" - - // this is for signing - flagName = "name" -) - -var ( - statusCmd = &cobra.Command{ - Use: "status", - Short: "Query remote node for status", - RunE: todoNotImplemented, - } -) - -// AddClientCommands returns a sub-tree of all basic client commands -// -// Call AddGetCommand and AddPostCommand to add custom txs and queries -func AddClientCommands(cmd *cobra.Command) { - cmd.AddCommand( - initClientCommand(), - statusCmd, - blockCommand(), - validatorCommand(), - lineBreak, - txSearchCommand(), - txCommand(), - lineBreak, - ) -} - -// GetCommands adds common flags to query commands -func GetCommands(cmds ...*cobra.Command) []*cobra.Command { - for _, c := range cmds { - c.Flags().Bool(flagTrustNode, false, "Don't verify proofs for responses") - } - return cmds -} - -// PostCommands adds common flags for commands to post tx -func PostCommands(cmds ...*cobra.Command) []*cobra.Command { - for _, c := range cmds { - c.Flags().String(flagName, "", "Name of private key with which to sign") - c.Flags().String(flagPassword, "", "Password to use the named private key") - } - return cmds -} - -func initClientCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "init", - Short: "Initialize light client", - RunE: todoNotImplemented, - } - cmd.Flags().StringP(flagChainID, "c", "", "ID of chain we connect to") - cmd.Flags().StringP(flagNode, "n", "tcp://localhost:46657", "Node to connect to") - cmd.Flags().String(flagGenesis, "", "Genesis file to verify header validity") - cmd.Flags().String(flagCommit, "", "File with trusted and signed header") - cmd.Flags().String(flagValHash, "", "Hash of trusted validator set (hex-encoded)") - return cmd -} - -func blockCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "block ", - Short: "Get verified data for a the block at given height", - RunE: todoNotImplemented, - } - cmd.Flags().StringSlice(flagSelect, []string{"header", "tx"}, "Fields to return (header|txs|results)") - return cmd -} - -func validatorCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "validatorset ", - Short: "Get the full validator set at given height", - RunE: todoNotImplemented, - } - return cmd -} - -func serveCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "serve", - Short: "Start LCD (light-client daemon), a local REST server", - RunE: todoNotImplemented, - } - // TODO: handle unix sockets also? - cmd.Flags().StringP(flagBind, "b", "localhost:1317", "Interface and port that server binds to") - cmd.Flags().String(flagCORS, "", "Set to domains that can make CORS requests (* for all)") - return cmd -} - -func txSearchCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "txs", - Short: "Search for all transactions that match the given tags", - RunE: todoNotImplemented, - } - cmd.Flags().StringSlice(flagTags, nil, "Tags that must match (may provide multiple)") - cmd.Flags().Bool(flagAny, false, "Return transactions that match ANY tag, rather than ALL") - return cmd -} - -func txCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "tx ", - Short: "Matches this txhash over all committed blocks", - RunE: todoNotImplemented, - } - return cmd -} diff --git a/examples/gaia/gaiacli/key.go b/examples/gaia/gaiacli/key.go deleted file mode 100644 index b3ffa323aed6..000000000000 --- a/examples/gaia/gaiacli/key.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import "github.com/spf13/cobra" - -const ( - flagPassword = "password" - flagNewPassword = "new-password" - flagType = "type" - flagSeed = "seed" - flagDryRun = "dry-run" -) - -var ( - listKeysCmd = &cobra.Command{ - Use: "list", - Short: "List all locally availably keys", - RunE: todoNotImplemented, - } - - showKeysCmd = &cobra.Command{ - Use: "show ", - Short: "Show key info for the given name", - RunE: todoNotImplemented, - } -) - -// KeyCommands registers a sub-tree of commands to interact with -// local private key storage. -func KeyCommands() *cobra.Command { - cmd := &cobra.Command{ - Use: "keys", - Short: "Add or view local private keys", - } - cmd.AddCommand( - addKeyCommand(), - listKeysCmd, - showKeysCmd, - lineBreak, - deleteKeyCommand(), - updateKeyCommand(), - ) - return cmd -} - -func addKeyCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "add ", - Short: "Create a new key, or import from seed", - RunE: todoNotImplemented, - } - cmd.Flags().StringP(flagPassword, "p", "", "Password to encrypt private key") - cmd.Flags().StringP(flagType, "t", "ed25519", "Type of private key (ed25519|secp256k1|ledger)") - cmd.Flags().StringP(flagSeed, "s", "", "Provide seed phrase to recover existing key instead of creating") - cmd.Flags().Bool(flagDryRun, false, "Perform action, but don't add key to local keystore") - return cmd -} - -func updateKeyCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "update ", - Short: "Change the password used to protect private key", - RunE: todoNotImplemented, - } - cmd.Flags().StringP(flagPassword, "p", "", "Current password to decrypt key") - cmd.Flags().String(flagNewPassword, "", "New password to use to protect key") - return cmd -} - -func deleteKeyCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "delete ", - Short: "Delete the given key", - RunE: todoNotImplemented, - } - cmd.Flags().StringP(flagPassword, "p", "", "Password of existing key to delete") - return cmd -} diff --git a/examples/gaia/gaiacli/main.go b/examples/gaia/gaiacli/main.go deleted file mode 100644 index 6321e25ee721..000000000000 --- a/examples/gaia/gaiacli/main.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "errors" - "os" - - "github.com/spf13/cobra" - - "github.com/tendermint/tmlibs/cli" -) - -const ( - flagTo = "to" - flagAmount = "amount" - flagFee = "fee" -) - -// gaiacliCmd is the entry point for this binary -var ( - gaiacliCmd = &cobra.Command{ - Use: "gaiacli", - Short: "Gaia light-client", - } - - lineBreak = &cobra.Command{Run: func(*cobra.Command, []string) {}} - - getAccountCmd = &cobra.Command{ - Use: "account
", - Short: "Query account balance", - RunE: todoNotImplemented, - } -) - -func todoNotImplemented(_ *cobra.Command, _ []string) error { - return errors.New("TODO: Command not yet implemented") -} - -func postSendCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "send", - Short: "Create and sign a send tx", - RunE: todoNotImplemented, - } - cmd.Flags().String(flagTo, "", "Address to send coins") - cmd.Flags().String(flagAmount, "", "Amount of coins to send") - cmd.Flags().String(flagFee, "", "Fee to pay along with transaction") - return cmd -} - -func main() { - // disable sorting - cobra.EnableCommandSorting = false - - // generic client commands - AddClientCommands(gaiacliCmd) - // query commands (custom to binary) - gaiacliCmd.AddCommand( - GetCommands(getAccountCmd)...) - // post tx commands (custom to binary) - gaiacliCmd.AddCommand( - PostCommands(postSendCommand())...) - - // add proxy, version and key info - gaiacliCmd.AddCommand( - lineBreak, - serveCommand(), - KeyCommands(), - lineBreak, - VersionCmd, - ) - - // prepare and add flags - executor := cli.PrepareBaseCmd(gaiacliCmd, "GA", os.ExpandEnv("$HOME/.gaiacli")) - executor.Execute() -} diff --git a/examples/gaia/gaiacli/version.go b/examples/gaia/gaiacli/version.go deleted file mode 100644 index 59fd799c07f0..000000000000 --- a/examples/gaia/gaiacli/version.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/cosmos/cosmos-sdk/version" -) - -var ( - // VersionCmd prints out the current sdk version - VersionCmd = &cobra.Command{ - Use: "version", - Short: "Print the app version", - Run: doVersionCmd, - } -) - -func doVersionCmd(cmd *cobra.Command, args []string) { - v := version.Version - if version.GitCommit != "" { - v = v + " " + version.GitCommit - } - fmt.Println(v) -} diff --git a/examples/gaia/gaiad/main.go b/examples/gaia/gaiad/main.go deleted file mode 100644 index ca9bb20dba30..000000000000 --- a/examples/gaia/gaiad/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "os" - - "github.com/spf13/cobra" - - "github.com/tendermint/tmlibs/cli" - - "github.com/cosmos/cosmos-sdk/baseapp" -) - -const ( - flagTo = "to" - flagAmount = "amount" - flagFee = "fee" -) - -// gaiadCmd is the entry point for this binary -var ( - gaiadCmd = &cobra.Command{ - Use: "gaiad", - Short: "Gaia Daemon (server)", - } -) - -func todoNotImplemented(_ *cobra.Command, _ []string) error { - return errors.New("TODO: Command not yet implemented") -} - -func main() { - // TODO: set this to something real - var node baseapp.BaseApp - - AddNodeCommands(gaiadCmd, node) - gaiadCmd.AddCommand( - VersionCmd, - ) - - // prepare and add flags - executor := cli.PrepareBaseCmd(gaiadCmd, "GA", os.ExpandEnv("$HOME/.gaiad")) - executor.Execute() -} diff --git a/examples/gaia/gaiad/node.go b/examples/gaia/gaiad/node.go deleted file mode 100644 index 8d362a6a7807..000000000000 --- a/examples/gaia/gaiad/node.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" - - "github.com/cosmos/cosmos-sdk/baseapp" -) - -const ( - flagWithTendermint = "with-tendermint" -) - -var ( - initNodeCmd = &cobra.Command{ - Use: "init ", - Short: "Initialize full node", - RunE: todoNotImplemented, - } - - resetNodeCmd = &cobra.Command{ - Use: "unsafe_reset_all", - Short: "Reset full node data (danger, must resync)", - RunE: todoNotImplemented, - } -) - -// AddNodeCommands registers all commands to interact -// with a local full-node as subcommands of the argument. -// -// Accept an application it should start -func AddNodeCommands(cmd *cobra.Command, node baseapp.BaseApp) { - cmd.AddCommand( - initNodeCmd, - startNodeCmd(node), - resetNodeCmd, - ) -} - -func startNodeCmd(node baseapp.BaseApp) *cobra.Command { - cmd := &cobra.Command{ - Use: "start", - Short: "Run the full node", - RunE: todoNotImplemented, - } - cmd.Flags().Bool(flagWithTendermint, true, "run abci app embedded in-process with tendermint") - return cmd -} diff --git a/glide.lock b/glide.lock index 51b7db0b86f1..f2fa12fefdf6 100644 --- a/glide.lock +++ b/glide.lock @@ -1,6 +1,8 @@ -hash: 2b4ad3bf1489a7cb5e62c6cb4c1fa976d4ae21993743e4968418c4e81925fb99 -updated: 2018-02-19T17:13:04.368106064Z +hash: fa45c8a4f5512ed730f793b93d4876bdc604a1333a5a1f938c98a0f7dd55f22e +updated: 2018-02-23T19:47:25.630102+01:00 imports: +- name: github.com/bgentry/speakeasy + version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd - name: github.com/btcsuite/btcd version: 50de9da05b50eb15658bb350f6ea24368a111ab7 subpackages: @@ -9,6 +11,8 @@ imports: version: 346938d642f2ec3594ed81d874461961cd0faa76 subpackages: - spew +- name: github.com/ebuchman/fail-test + version: 95f809107225be108efcf10a3509e4ea6ceef3c4 - name: github.com/fsnotify/fsnotify version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9 - name: github.com/go-kit/kit @@ -40,6 +44,8 @@ imports: - ptypes/timestamp - name: github.com/golang/snappy version: 553a641470496b2327abcac10b36396bd98e45c9 +- name: github.com/gorilla/websocket + version: ea4d1f681babbce9545c9c5f3d5194a789c89f5b - name: github.com/hashicorp/hcl version: 23c074d0eceb2b8a5bfdbb271ab780cde70f05a8 subpackages: @@ -51,6 +57,8 @@ imports: - json/parser - json/scanner - json/token +- name: github.com/howeyc/crc16 + version: 96a97a1abb579c7ff1a8ffa77f2e72d1c314b57f - name: github.com/inconshreveable/mousetrap version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 - name: github.com/jmhodges/levigo @@ -59,12 +67,16 @@ imports: version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 - name: github.com/magiconair/properties version: 49d762b9817ba1c2e9d0c69183c2b4a8b8f1d934 +- name: github.com/mattn/go-isatty + version: 0360b2af4f38e8d38c7fce2a9f4e702702d73a39 - name: github.com/mitchellh/mapstructure version: b4575eea38cca1123ec2dc90c26529b5c5acfcff - name: github.com/pelletier/go-toml version: acdc4509485b587f5e675510c4f2c63e90ff68a8 - name: github.com/pkg/errors version: 645ef00459ed84a119197bfb8d8205042c6df63d +- name: github.com/rcrowley/go-metrics + version: 1f30fe9094a513ce4c700b9a54458bbb0c96996c - name: github.com/rigelrozanski/common version: f691f115798593d783b9999b1263c2f4ffecc439 - name: github.com/spf13/afero @@ -97,8 +109,11 @@ imports: - leveldb/table - leveldb/util - name: github.com/tendermint/abci - version: c960c5275617ef141c92c3d7fc65a396c97662df + version: 68592f4d8ee34e97db94b7a7976b1309efdb7eb9 subpackages: + - client + - example/code + - example/dummy - server - types - name: github.com/tendermint/ed25519 @@ -108,21 +123,72 @@ imports: - extra25519 - name: github.com/tendermint/go-crypto version: 4fc3055dbd17aa1203d0abc64b9293f378da22ec + subpackages: + - keys + - keys/bcrypt + - keys/words + - keys/words/wordlist - name: github.com/tendermint/go-wire version: 5d7845f24b843c914cf571dad2ca13c91cf70f0d - name: github.com/tendermint/iavl version: 1a59ec0c82dc940c25339dd7c834df5cb76a95cb +- name: github.com/tendermint/tendermint + version: 6947e118f54e4df24f5e2c79bcdd66199e54d885 + subpackages: + - blockchain + - cmd/tendermint/commands + - config + - consensus + - consensus/types + - evidence + - lite + - lite/client + - lite/errors + - lite/files + - lite/proxy + - mempool + - node + - p2p + - p2p/conn + - p2p/pex + - p2p/trust + - p2p/upnp + - proxy + - rpc/client + - rpc/core + - rpc/core/types + - rpc/grpc + - rpc/lib + - rpc/lib/client + - rpc/lib/server + - rpc/lib/types + - state + - state/txindex + - state/txindex/kv + - state/txindex/null + - types + - version + - wire - name: github.com/tendermint/tmlibs - version: a0f652dc2e131be86fc8d9e4e2beec9831a8a6ec + version: 1b9b5652a199ab0be2e781393fb275b66377309d subpackages: + - autofile - cli + - cli/flags + - clist - common - db + - flowrate - log - merkle + - pubsub + - pubsub/query - name: golang.org/x/crypto version: 1875d0a70c90e57f11972aefd42276df65e895b9 subpackages: + - blowfish + - curve25519 + - nacl/box - nacl/secretbox - openpgp/armor - openpgp/errors diff --git a/glide.yaml b/glide.yaml index 276513b2cebc..086cd0c25cfc 100644 --- a/glide.yaml +++ b/glide.yaml @@ -4,6 +4,10 @@ import: version: ^1.0.0 subpackages: - proto +- package: github.com/bgentry/speakeasy + version: ^0.1.0 +- package: github.com/mattn/go-isatty + version: ~0.0.3 - package: github.com/pkg/errors version: ^0.8.0 - package: github.com/rigelrozanski/common @@ -25,6 +29,14 @@ import: - db - log - merkle +- package: github.com/tendermint/tendermint + version: breaking/wire-sdk2 + subpackages: + - cmd/tendermint/commands + - config + - lite + - rpc/client + - types - package: golang.org/x/crypto subpackages: - ripemd160 diff --git a/mock/app.go b/mock/app.go new file mode 100644 index 000000000000..3e9a3ad5f567 --- /dev/null +++ b/mock/app.go @@ -0,0 +1,122 @@ +package mock + +import ( + "encoding/json" + "fmt" + + abci "github.com/tendermint/abci/types" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" + + bam "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NewApp creates a simple mock kvstore app for testing. +// It should work similar to a real app. +// Make sure rootDir is empty before running the test, +// in order to guarantee consistent results +func NewApp(rootDir string, logger log.Logger) (abci.Application, error) { + db, err := dbm.NewGoLevelDB("mock", rootDir) + if err != nil { + return nil, err + } + + // Capabilities key to access the main KVStore. + capKeyMainStore := sdk.NewKVStoreKey("main") + + // Create BaseApp. + baseApp := bam.NewBaseApp("kvstore", logger, db) + + // Set mounts for BaseApp's MultiStore. + baseApp.MountStoresIAVL(capKeyMainStore) + + // Set Tx decoder + baseApp.SetTxDecoder(decodeTx) + + baseApp.SetInitChainer(InitChainer(capKeyMainStore)) + + // Set a handler Route. + baseApp.Router().AddRoute("kvstore", KVStoreHandler(capKeyMainStore)) + + // Load latest version. + if err := baseApp.LoadLatestVersion(capKeyMainStore); err != nil { + return nil, err + } + + return baseApp, nil +} + +// KVStoreHandler is a simple handler that takes kvstoreTx and writes +// them to the db +func KVStoreHandler(storeKey sdk.StoreKey) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + dTx, ok := msg.(kvstoreTx) + if !ok { + panic("KVStoreHandler should only receive kvstoreTx") + } + + // tx is already unmarshalled + key := dTx.key + value := dTx.value + + store := ctx.KVStore(storeKey) + store.Set(key, value) + + return sdk.Result{ + Code: 0, + Log: fmt.Sprintf("set %s=%s", key, value), + } + } +} + +// basic KV structure +type KV struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// What Genesis JSON is formatted as +type GenesisJSON struct { + Values []KV `json:"values"` +} + +// InitChainer returns a function that can initialize the chain +// with key/value pairs +func InitChainer(key sdk.StoreKey) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { + return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + stateJSON := req.AppStateBytes + + genesisState := new(GenesisJSON) + err := json.Unmarshal(stateJSON, genesisState) + if err != nil { + panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 + // return sdk.ErrGenesisParse("").TraceCause(err, "") + } + + for _, val := range genesisState.Values { + store := ctx.KVStore(key) + store.Set([]byte(val.Key), []byte(val.Value)) + } + return abci.ResponseInitChain{} + } +} + +// GenInitOptions can be passed into InitCmd, +// returns a static string of a few key-values that can be parsed +// by InitChainer +func GenInitOptions(args []string) (json.RawMessage, error) { + opts := []byte(`{ + "values": [ + { + "key": "hello", + "value": "goodbye" + }, + { + "key": "foo", + "value": "bar" + } + ] +}`) + return opts, nil +} diff --git a/mock/app_test.go b/mock/app_test.go new file mode 100644 index 000000000000..103530e5014c --- /dev/null +++ b/mock/app_test.go @@ -0,0 +1,75 @@ +package mock + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/abci/types" +) + +// TestInitApp makes sure we can initialize this thing without an error +func TestInitApp(t *testing.T) { + // set up an app + app, closer, err := SetupApp() + + // closer may need to be run, even when error in later stage + if closer != nil { + defer closer() + } + require.NoError(t, err) + + // initialize it future-way + opts, err := GenInitOptions(nil) + require.NoError(t, err) + req := abci.RequestInitChain{AppStateBytes: opts} + app.InitChain(req) + app.Commit() + + // XXX test failing + // make sure we can query these values + query := abci.RequestQuery{ + Path: "/main/key", + Data: []byte("foo"), + } + qres := app.Query(query) + require.Equal(t, uint32(0), qres.Code, qres.Log) + assert.Equal(t, []byte("bar"), qres.Value) +} + +// TextDeliverTx ensures we can write a tx +func TestDeliverTx(t *testing.T) { + // set up an app + app, closer, err := SetupApp() + // closer may need to be run, even when error in later stage + if closer != nil { + defer closer() + } + require.NoError(t, err) + + key := "my-special-key" + value := "top-secret-data!!" + tx := NewTx(key, value) + txBytes := tx.GetSignBytes() + + header := abci.Header{ + AppHash: []byte("apphash"), + Height: 1, + } + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + dres := app.DeliverTx(txBytes) + require.Equal(t, uint32(0), dres.Code, dres.Log) + app.EndBlock(abci.RequestEndBlock{}) + cres := app.Commit() + require.NotEmpty(t, cres.Data) + + // make sure we can query these values + query := abci.RequestQuery{ + Path: "/main/key", + Data: []byte(key), + } + qres := app.Query(query) + require.Equal(t, uint32(0), qres.Code, qres.Log) + assert.Equal(t, []byte(value), qres.Value) +} diff --git a/mock/helpers.go b/mock/helpers.go new file mode 100644 index 000000000000..601fee897dfd --- /dev/null +++ b/mock/helpers.go @@ -0,0 +1,27 @@ +package mock + +import ( + "io/ioutil" + "os" + + abci "github.com/tendermint/abci/types" + "github.com/tendermint/tmlibs/log" +) + +// SetupApp returns an application as well as a clean-up function +// to be used to quickly setup a test case with an app +func SetupApp() (abci.Application, func(), error) { + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). + With("module", "mock") + rootDir, err := ioutil.TempDir("", "mock-sdk") + if err != nil { + return nil, nil, err + } + + cleanup := func() { + os.RemoveAll(rootDir) + } + + app, err := NewApp(rootDir, logger) + return app, cleanup, err +} diff --git a/mock/tx.go b/mock/tx.go new file mode 100644 index 000000000000..a9adff5deed8 --- /dev/null +++ b/mock/tx.go @@ -0,0 +1,89 @@ +//nolint +package mock + +import ( + "bytes" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + crypto "github.com/tendermint/go-crypto" +) + +// An sdk.Tx which is its own sdk.Msg. +type kvstoreTx struct { + key []byte + value []byte + bytes []byte +} + +var _ sdk.Tx = kvstoreTx{} + +func NewTx(key, value string) kvstoreTx { + bytes := fmt.Sprintf("%s=%s", key, value) + return kvstoreTx{ + key: []byte(key), + value: []byte(value), + bytes: []byte(bytes), + } +} + +func (tx kvstoreTx) Get(key interface{}) (value interface{}) { + switch k := key.(type) { + case string: + switch k { + case "key": + return tx.key + case "value": + return tx.value + } + } + return nil +} + +func (tx kvstoreTx) Type() string { + return "kvstore" +} + +func (tx kvstoreTx) GetMsg() sdk.Msg { + return tx +} + +func (tx kvstoreTx) GetSignBytes() []byte { + return tx.bytes +} + +// Should the app be calling this? Or only handlers? +func (tx kvstoreTx) ValidateBasic() sdk.Error { + return nil +} + +func (tx kvstoreTx) GetSigners() []crypto.Address { + return nil +} + +func (tx kvstoreTx) GetSignatures() []sdk.StdSignature { + return nil +} + +func (tx kvstoreTx) GetFeePayer() crypto.Address { + return nil +} + +// takes raw transaction bytes and decodes them into an sdk.Tx. An sdk.Tx has +// all the signatures and can be used to authenticate. +func decodeTx(txBytes []byte) (sdk.Tx, sdk.Error) { + var tx sdk.Tx + + split := bytes.Split(txBytes, []byte("=")) + if len(split) == 1 { + k := split[0] + tx = kvstoreTx{k, k, txBytes} + } else if len(split) == 2 { + k, v := split[0], split[1] + tx = kvstoreTx{k, v, txBytes} + } else { + return nil, sdk.ErrTxParse("too many =") + } + + return tx, nil +} diff --git a/server/init.go b/server/init.go new file mode 100644 index 000000000000..4dc724587336 --- /dev/null +++ b/server/init.go @@ -0,0 +1,182 @@ +package server + +import ( + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/spf13/cobra" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-crypto/keys/words" + cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" + + tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + cfg "github.com/tendermint/tendermint/config" + tmtypes "github.com/tendermint/tendermint/types" +) + +// InitCmd will initialize all files for tendermint, +// along with proper app_options. +// The application can pass in a function to generate +// proper options. And may want to use GenerateCoinKey +// to create default account(s). +func InitCmd(gen GenOptions, logger log.Logger) *cobra.Command { + cmd := initCmd{ + gen: gen, + logger: logger, + } + return &cobra.Command{ + Use: "init", + Short: "Initialize genesis files", + RunE: cmd.run, + } +} + +// GenOptions can parse command-line and flag to +// generate default app_options for the genesis file. +// This is application-specific +type GenOptions func(args []string) (json.RawMessage, error) + +// GenerateCoinKey returns the address of a public key, +// along with the secret phrase to recover the private key. +// You can give coins to this address and return the recovery +// phrase to the user to access them. +func GenerateCoinKey() (crypto.Address, string, error) { + // construct an in-memory key store + codec, err := words.LoadCodec("english") + if err != nil { + return nil, "", err + } + keybase := keys.New( + dbm.NewMemDB(), + codec, + ) + + // generate a private key, with recovery phrase + info, secret, err := keybase.Create("name", "pass", keys.AlgoEd25519) + if err != nil { + return nil, "", err + } + + addr := info.PubKey.Address() + return addr, secret, nil +} + +type initCmd struct { + gen GenOptions + logger log.Logger +} + +func (c initCmd) run(cmd *cobra.Command, args []string) error { + // Run the basic tendermint initialization, + // set up a default genesis with no app_options + config, err := tcmd.ParseConfig() + if err != nil { + return err + } + err = c.initTendermintFiles(config) + if err != nil { + return err + } + + // no app_options, leave like tendermint + if c.gen == nil { + return nil + } + + // Now, we want to add the custom app_options + options, err := c.gen(args) + if err != nil { + return err + } + + // And add them to the genesis file + genFile := config.GenesisFile() + return addGenesisOptions(genFile, options) +} + +// This was copied from tendermint/cmd/tendermint/commands/init.go +// so we could pass in the config and the logger. +func (c initCmd) initTendermintFiles(config *cfg.Config) error { + // private validator + privValFile := config.PrivValidatorFile() + var privValidator *tmtypes.PrivValidatorFS + if cmn.FileExists(privValFile) { + privValidator = tmtypes.LoadPrivValidatorFS(privValFile) + c.logger.Info("Found private validator", "path", privValFile) + } else { + privValidator = tmtypes.GenPrivValidatorFS(privValFile) + privValidator.Save() + c.logger.Info("Generated private validator", "path", privValFile) + } + + // genesis file + genFile := config.GenesisFile() + if cmn.FileExists(genFile) { + c.logger.Info("Found genesis file", "path", genFile) + } else { + genDoc := tmtypes.GenesisDoc{ + ChainID: cmn.Fmt("test-chain-%v", cmn.RandStr(6)), + } + genDoc.Validators = []tmtypes.GenesisValidator{{ + PubKey: privValidator.GetPubKey(), + Power: 10, + }} + + if err := genDoc.SaveAs(genFile); err != nil { + return err + } + c.logger.Info("Generated genesis file", "path", genFile) + } + return nil +} + +// GenesisDoc involves some tendermint-specific structures we don't +// want to parse, so we just grab it into a raw object format, +// so we can add one line. +type GenesisDoc map[string]json.RawMessage + +func addGenesisOptions(filename string, options json.RawMessage) error { + bz, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + + var doc GenesisDoc + err = json.Unmarshal(bz, &doc) + if err != nil { + return err + } + + doc["app_state"] = options + out, err := json.MarshalIndent(doc, "", " ") + if err != nil { + return err + } + + return ioutil.WriteFile(filename, out, 0600) +} + +// GetGenesisJSON returns a new tendermint genesis with Basecoin app_options +// that grant a large amount of "mycoin" to a single address +// TODO: A better UX for generating genesis files +func GetGenesisJSON(pubkey, chainID, denom, addr string, options string) string { + return fmt.Sprintf(`{ + "accounts": [{ + "address": "%s", + "coins": [ + { + "denom": "%s", + "amount": 9007199254740992 + } + ] + }], + "plugin_options": [ + "coin/issuer", {"app": "sigs", "addr": "%s"}%s + ] +}`, addr, denom, addr, options) +} diff --git a/server/init_test.go b/server/init_test.go new file mode 100644 index 000000000000..0af1ecc11823 --- /dev/null +++ b/server/init_test.go @@ -0,0 +1,36 @@ +package server + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/tendermint/tmlibs/log" + + "github.com/cosmos/cosmos-sdk/mock" +) + +// setupViper creates a homedir to run inside, +// and returns a cleanup function to defer +func setupViper() func() { + rootDir, err := ioutil.TempDir("", "mock-sdk-cmd") + if err != nil { + panic(err) // fuck it! + } + viper.Set("home", rootDir) + return func() { + os.RemoveAll(rootDir) + } +} + +func TestInit(t *testing.T) { + defer setupViper()() + + logger := log.NewNopLogger() + cmd := InitCmd(mock.GenInitOptions, logger) + err := cmd.RunE(nil, nil) + require.NoError(t, err) +} diff --git a/server/reset.go b/server/reset.go new file mode 100644 index 000000000000..5c70bbdace14 --- /dev/null +++ b/server/reset.go @@ -0,0 +1,31 @@ +package server + +import ( + "github.com/spf13/cobra" + + tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + "github.com/tendermint/tmlibs/log" +) + +// UnsafeResetAllCmd - extension of the tendermint command, resets initialization +func UnsafeResetAllCmd(logger log.Logger) *cobra.Command { + cmd := resetAll{logger} + return &cobra.Command{ + Use: "unsafe_reset_all", + Short: "Reset all blockchain data", + RunE: cmd.run, + } +} + +type resetAll struct { + logger log.Logger +} + +func (r resetAll) run(cmd *cobra.Command, args []string) error { + cfg, err := tcmd.ParseConfig() + if err != nil { + return err + } + tcmd.ResetAll(cfg.DBDir(), cfg.PrivValidatorFile(), r.logger) + return nil +} diff --git a/server/start.go b/server/start.go new file mode 100644 index 000000000000..1424c81532da --- /dev/null +++ b/server/start.go @@ -0,0 +1,119 @@ +package server + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/abci/server" + abci "github.com/tendermint/abci/types" + + tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" + "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/proxy" + "github.com/tendermint/tendermint/types" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" +) + +const ( + flagWithTendermint = "with-tendermint" + flagAddress = "address" +) + +// appGenerator lets us lazily initialize app, using home dir +// and other flags (?) to start +type appGenerator func(string, log.Logger) (abci.Application, error) + +// StartCmd runs the service passed in, either +// stand-alone, or in-process with tendermint +func StartCmd(app appGenerator, logger log.Logger) *cobra.Command { + start := startCmd{ + app: app, + logger: logger, + } + cmd := &cobra.Command{ + Use: "start", + Short: "Run the full node", + RunE: start.run, + } + // basic flags for abci app + cmd.Flags().Bool(flagWithTendermint, true, "run abci app embedded in-process with tendermint") + cmd.Flags().String(flagAddress, "tcp://0.0.0.0:46658", "Listen address") + + // AddNodeFlags adds support for all + // tendermint-specific command line options + tcmd.AddNodeFlags(cmd) + return cmd +} + +type startCmd struct { + app appGenerator + logger log.Logger +} + +func (s startCmd) run(cmd *cobra.Command, args []string) error { + if !viper.GetBool(flagWithTendermint) { + s.logger.Info("Starting ABCI without Tendermint") + return s.startStandAlone() + } + s.logger.Info("Starting ABCI with Tendermint") + return s.startInProcess() +} + +func (s startCmd) startStandAlone() error { + // Generate the app in the proper dir + addr := viper.GetString(flagAddress) + home := viper.GetString("home") + app, err := s.app(home, s.logger) + if err != nil { + return err + } + + svr, err := server.NewServer(addr, "socket", app) + if err != nil { + return errors.Errorf("Error creating listener: %v\n", err) + } + svr.SetLogger(s.logger.With("module", "abci-server")) + svr.Start() + + // Wait forever + cmn.TrapSignal(func() { + // Cleanup + svr.Stop() + }) + return nil +} + +func (s startCmd) startInProcess() error { + cfg, err := tcmd.ParseConfig() + if err != nil { + return err + } + + home := cfg.RootDir + app, err := s.app(home, s.logger) + if err != nil { + return err + } + + // Create & start tendermint node + n, err := node.NewNode(cfg, + types.LoadOrGenPrivValidatorFS(cfg.PrivValidatorFile()), + proxy.NewLocalClientCreator(app), + node.DefaultGenesisDocProviderFunc(cfg), + node.DefaultDBProvider, + s.logger.With("module", "node")) + if err != nil { + return err + } + + err = n.Start() + if err != nil { + return err + } + + // Trap signal, run forever. + n.RunForever() + return nil +} diff --git a/server/start_test.go b/server/start_test.go new file mode 100644 index 000000000000..5b7ab3e76c40 --- /dev/null +++ b/server/start_test.go @@ -0,0 +1,72 @@ +package server + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/mock" + "github.com/tendermint/tmlibs/log" +) + +func TestStartStandAlone(t *testing.T) { + defer setupViper()() + + logger := log.NewNopLogger() + initCmd := InitCmd(mock.GenInitOptions, logger) + err := initCmd.RunE(nil, nil) + require.NoError(t, err) + + // set up app and start up + viper.Set(flagWithTendermint, false) + viper.Set(flagAddress, "localhost:11122") + startCmd := StartCmd(mock.NewApp, logger) + timeout := time.Duration(3) * time.Second + + err = runOrTimeout(startCmd, timeout) + require.NoError(t, err) +} + +func TestStartWithTendermint(t *testing.T) { + defer setupViper()() + + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). + With("module", "mock-cmd") + // logger := log.NewNopLogger() + initCmd := InitCmd(mock.GenInitOptions, logger) + err := initCmd.RunE(nil, nil) + require.NoError(t, err) + + // set up app and start up + viper.Set(flagWithTendermint, true) + startCmd := StartCmd(mock.NewApp, logger) + timeout := time.Duration(3) * time.Second + + err = runOrTimeout(startCmd, timeout) + require.NoError(t, err) +} + +func runOrTimeout(cmd *cobra.Command, timeout time.Duration) error { + done := make(chan error) + go func(out chan<- error) { + // this should NOT exit + err := cmd.RunE(nil, nil) + if err != nil { + out <- err + } + out <- fmt.Errorf("start died for unknown reasons") + }(done) + timer := time.NewTimer(timeout) + + select { + case err := <-done: + return err + case <-timer.C: + return nil + } +} diff --git a/tests/check_basecli.sh b/tests/check_basecli.sh new file mode 100755 index 000000000000..ec2c458cb6a8 --- /dev/null +++ b/tests/check_basecli.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +# Note: Bucky, I know you want to kill bash tests. +# Please show me how to do an alternative to this. +# I would rather get code running before I leave than +# fight trying to invent some new test harness that +# no one else will understand. +# +# Thus, I leave this as an exercise to the reader to +# port into a non-bash version. And I don't do it proper... +# just automate my manual tests + +# WARNING!!! +rm -rf ~/.basecoind ~/.basecli +cd $GOPATH/src/github.com/cosmos/cosmos-sdk +# make get_vendor_deps +make build + +# init stuff +SEED=`./build/basecoind init | tail -1` +PASS='some-silly-123' +(echo $PASS; echo $SEED) | ./build/basecli keys add demo --recover +ADDR=`./build/basecli keys show demo | cut -f3` +echo "Recovered seed for demo:" $ADDR + +# start up server +./build/basecoind start > ~/.basecoind/basecoind.log 2>&1 & +sleep 5 +PID_SERVER=$! + +# query original state +TO='ABCAFE00DEADBEEF00CAFE00DEADBEEF00CAFE00' +echo; echo "My account:" $ADDR +./build/basecli account $ADDR +echo; echo "Empty account:" $TO +./build/basecli account $TO + +# send some money +TX=`echo $PASS | ./build/basecli send --to=$TO --amount=1000mycoin --name=demo --seq=0` +echo; echo "SendTx"; echo $TX +HASH=`echo $TX | cut -d' ' -f6` +echo "tx hash:" $HASH + +# let some blocks come up.... +./build/basecli status | jq .latest_block_height +sleep 2 +./build/basecli status | jq .latest_block_height + +# balances change +echo; echo "My account went down" +./build/basecli account $ADDR +echo; echo "Empty account got some cash" +./build/basecli account $TO + +# query original tx +echo; echo "View tx" +./build/basecli tx $HASH + +# wait a bit then dump out some blockchain state +sleep 10 +./build/basecli status --trace +./build/basecli block --trace +./build/basecli validatorset --trace + +# shutdown, but add a sleep if you want to manually run some cli scripts +# against this server before it goes away +# sleep 120 +kill $PID_SERVER + diff --git a/tools/Makefile b/tools/Makefile index 39611f305009..6786575cdc88 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -40,10 +40,10 @@ install: get_vendor_deps @echo "$(ansi_grn)Installing tools$(ansi_end)" @echo "$(ansi_yel)Install go-vendorinstall$(ansi_end)" go build -o bin/go-vendorinstall go-vendorinstall/*.go - + @echo "$(ansi_yel)Install gometalinter.v2$(ansi_end)" - GOBIN=$(CURDIR)/bin ./bin/go-vendorinstall gopkg.in/alecthomas/gometalinter.v2 - + GOBIN=$(CURDIR)/bin ./bin/go-vendorinstall github.com/alecthomas/gometalinter + @echo "$(ansi_yel)Install shelldown$(ansi_end)" GOBIN=$(CURDIR)/bin ./bin/go-vendorinstall github.com/rigelrozanski/shelldown/cmd/shelldown diff --git a/tools/glide.lock b/tools/glide.lock index db718da50d1c..f4af28eeccab 100644 --- a/tools/glide.lock +++ b/tools/glide.lock @@ -1,8 +1,8 @@ -hash: a163b1c4806024cfc9062db75a0abed285ec40461243e59af0e147db2c4bf0ce -updated: 2018-01-15T19:02:49.834182027-08:00 +hash: 934ad5be72c9c240e8555eb6e1b2319840266c04c0fa9e024008cf841c0cee65 +updated: 2018-02-23T19:33:08.596187+01:00 imports: -- name: github.com/inconshreveable/mousetrap - version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/alecthomas/gometalinter + version: 46cc1ea3778b247666c2949669a3333c532fa9c6 - name: github.com/rigelrozanski/common version: f691f115798593d783b9999b1263c2f4ffecc439 - name: github.com/rigelrozanski/shelldown @@ -12,7 +12,7 @@ imports: - name: github.com/spf13/cobra version: 7b2c5ac9fc04fc5efafb60700713d4fa609b777b - name: github.com/spf13/pflag - version: 97afa5e7ca8a08a383cb259e06636b5e2cc7897f -- name: gopkg.in/alecthomas/gometalinter.v2 - version: 88d47c66988c5a5cb3945925da47c883800a94df + version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 +- name: github.com/spf13/viper + version: 25b30aa063fc18e48662b86996252eabdcf2f0c7 testImports: [] diff --git a/tools/glide.yaml b/tools/glide.yaml index 3a3e85d6a751..6ddd6443daef 100644 --- a/tools/glide.yaml +++ b/tools/glide.yaml @@ -1,6 +1,13 @@ package: github.com/cosmos/cosmos-sdk/tools import: +- package: github.com/alecthomas/gometalinter + version: ^2.0.5 - package: github.com/rigelrozanski/shelldown subpackages: - cmd/shelldown -- package: gopkg.in/alecthomas/gometalinter.v2 +- package: github.com/spf13/pflag + version: v1.0.0 +- package: github.com/spf13/cobra + version: v0.0.1 +- package: github.com/spf13/viper + version: ^1.0.0 diff --git a/types/account.go b/types/account.go index d2dd9df6cf66..3a6e5931f000 100644 --- a/types/account.go +++ b/types/account.go @@ -30,3 +30,6 @@ type AccountMapper interface { GetAccount(ctx Context, addr crypto.Address) Account SetAccount(ctx Context, acc Account) } + +// Application function variable used to unmarshal account +type ParseAccount func([]byte) (Account, error) diff --git a/types/signature.go b/types/signature.go index 7fecc5fe96a6..5bca2f6069ee 100644 --- a/types/signature.go +++ b/types/signature.go @@ -4,7 +4,7 @@ import crypto "github.com/tendermint/go-crypto" // Standard Signature type StdSignature struct { - crypto.PubKey // optional - crypto.Signature - Sequence int64 + crypto.PubKey `json:"pub_key"` // optional + crypto.Signature `json:"signature"` + Sequence int64 `json:"sequence"` } diff --git a/types/tx_msg.go b/types/tx_msg.go index 8763075b8bd0..1a7f63f938a0 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -52,8 +52,8 @@ var _ Tx = (*StdTx)(nil) // StdTx is a standard way to wrap a Msg with Signatures. // NOTE: the first signature is the FeePayer (Signatures must not be nil). type StdTx struct { - Msg - Signatures []StdSignature + Msg `json:"msg"` + Signatures []StdSignature `json:"signatures"` } func NewStdTx(msg Msg, sigs []StdSignature) StdTx { diff --git a/examples/gaia/gaiad/version.go b/version/command.go similarity index 67% rename from examples/gaia/gaiad/version.go rename to version/command.go index 59fd799c07f0..9986f605d736 100644 --- a/examples/gaia/gaiad/version.go +++ b/version/command.go @@ -1,11 +1,9 @@ -package main +package version import ( "fmt" "github.com/spf13/cobra" - - "github.com/cosmos/cosmos-sdk/version" ) var ( @@ -18,9 +16,9 @@ var ( ) func doVersionCmd(cmd *cobra.Command, args []string) { - v := version.Version - if version.GitCommit != "" { - v = v + " " + version.GitCommit + v := Version + if GitCommit != "" { + v = v + " " + GitCommit } fmt.Println(v) } diff --git a/x/auth/commands/account.go b/x/auth/commands/account.go new file mode 100644 index 000000000000..613442d720d9 --- /dev/null +++ b/x/auth/commands/account.go @@ -0,0 +1,82 @@ +package commands + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" + + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +// GetAccountCmd for the auth.BaseAccount type +func GetAccountCmdDefault(storeName string, cdc *wire.Codec) *cobra.Command { + return GetAccountCmd(storeName, cdc, getParseAccount(cdc)) +} + +func getParseAccount(cdc *wire.Codec) sdk.ParseAccount { + return func(accBytes []byte) (sdk.Account, error) { + acct := new(auth.BaseAccount) + err := cdc.UnmarshalBinary(accBytes, acct) + return acct, err + } +} + +// GetAccountCmd returns a query account that will display the +// state of the account at a given address +func GetAccountCmd(storeName string, cdc *wire.Codec, parser sdk.ParseAccount) *cobra.Command { + cmdr := commander{ + storeName, + cdc, + parser, + } + return &cobra.Command{ + Use: "account
", + Short: "Query account balance", + RunE: cmdr.getAccountCmd, + } +} + +type commander struct { + storeName string + cdc *wire.Codec + parser sdk.ParseAccount +} + +func (c commander) getAccountCmd(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide an account name") + } + + // find the key to look up the account + addr := args[0] + bz, err := hex.DecodeString(addr) + if err != nil { + return err + } + key := crypto.Address(bz) + + res, err := client.Query(key, c.storeName) + + // parse out the value + account, err := c.parser(res) + if err != nil { + return err + } + + // print out whole account + output, err := json.MarshalIndent(account, "", " ") + if err != nil { + return err + } + fmt.Println(string(output)) + + return nil +} diff --git a/x/bank/commands/sendtx.go b/x/bank/commands/sendtx.go new file mode 100644 index 000000000000..941b0b69f7f8 --- /dev/null +++ b/x/bank/commands/sendtx.go @@ -0,0 +1,129 @@ +package commands + +import ( + "encoding/hex" + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/keys" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/cosmos-sdk/x/bank" +) + +const ( + flagTo = "to" + flagAmount = "amount" + flagFee = "fee" + flagSequence = "seq" +) + +// SendTxCommand will create a send tx and sign it with the given key +func SendTxCmd(cdc *wire.Codec) *cobra.Command { + cmdr := commander{cdc} + cmd := &cobra.Command{ + Use: "send", + Short: "Create and sign a send tx", + RunE: cmdr.sendTxCmd, + } + cmd.Flags().String(flagTo, "", "Address to send coins") + cmd.Flags().String(flagAmount, "", "Amount of coins to send") + cmd.Flags().String(flagFee, "", "Fee to pay along with transaction") + cmd.Flags().Int64(flagSequence, 0, "Sequence number to sign the tx") + return cmd +} + +type commander struct { + cdc *wire.Codec +} + +func (c commander) sendTxCmd(cmd *cobra.Command, args []string) error { + txBytes, err := c.buildTx() + if err != nil { + return err + } + + res, err := client.BroadcastTx(txBytes) + if err != nil { + return err + } + + fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String()) + return nil +} + +func (c commander) buildTx() ([]byte, error) { + keybase, err := keys.GetKeyBase() + if err != nil { + return nil, err + } + + name := viper.GetString(client.FlagName) + info, err := keybase.Get(name) + if err != nil { + return nil, errors.Errorf("No key for: %s", name) + } + from := info.PubKey.Address() + + msg, err := buildMsg(from) + if err != nil { + return nil, err + } + + // sign and build + bz := msg.GetSignBytes() + buf := client.BufferStdin() + prompt := fmt.Sprintf("Password to sign with '%s':", name) + passphrase, err := client.GetPassword(prompt, buf) + if err != nil { + return nil, err + } + sig, pubkey, err := keybase.Sign(name, passphrase, bz) + if err != nil { + return nil, err + } + sigs := []sdk.StdSignature{{ + PubKey: pubkey, + Signature: sig, + Sequence: viper.GetInt64(flagSequence), + }} + + // marshal bytes + tx := sdk.NewStdTx(msg, sigs) + + txBytes, err := c.cdc.MarshalBinary(tx) + if err != nil { + return nil, err + } + return txBytes, nil +} + +func buildMsg(from crypto.Address) (sdk.Msg, error) { + + // parse coins + amount := viper.GetString(flagAmount) + coins, err := sdk.ParseCoins(amount) + if err != nil { + return nil, err + } + + // parse destination address + dest := viper.GetString(flagTo) + bz, err := hex.DecodeString(dest) + if err != nil { + return nil, err + } + to := crypto.Address(bz) + + input := bank.NewInput(from, coins) + output := bank.NewOutput(to, coins) + msg := bank.NewSendMsg([]bank.Input{input}, []bank.Output{output}) + return msg, nil +} diff --git a/x/bank/handler.go b/x/bank/handler.go index e035818cdb92..b5d2a675724e 100644 --- a/x/bank/handler.go +++ b/x/bank/handler.go @@ -39,6 +39,7 @@ func handleSendMsg(ctx sdk.Context, ck CoinKeeper, msg SendMsg) sdk.Result { } } + // TODO: add some tags so we can search it! return sdk.Result{} // TODO }