From cddb501f5cacd061746df01eb4e852690b06bfa0 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 14 Aug 2021 21:40:16 +0800 Subject: [PATCH] Added command to generate chain link JSON See PR #583 --- CHANGELOG.md | 1 + app/desmos/cmd/chainlink/create_json.go | 101 ++++++++++ app/desmos/cmd/chainlink/create_json_test.go | 83 ++++++++ app/desmos/cmd/chainlink/types/chain.go | 19 ++ app/desmos/cmd/chainlink/types/config.go | 19 ++ app/desmos/cmd/chainlink/types/getter.go | 194 +++++++++++++++++++ app/desmos/cmd/root.go | 14 +- app/desmos/cmd/{ => sign}/sign.go | 2 +- app/desmos/cmd/{ => sign}/sign_test.go | 6 +- go.mod | 1 + go.sum | 13 ++ 11 files changed, 444 insertions(+), 9 deletions(-) create mode 100644 app/desmos/cmd/chainlink/create_json.go create mode 100644 app/desmos/cmd/chainlink/create_json_test.go create mode 100644 app/desmos/cmd/chainlink/types/chain.go create mode 100644 app/desmos/cmd/chainlink/types/config.go create mode 100644 app/desmos/cmd/chainlink/types/getter.go rename app/desmos/cmd/{ => sign}/sign.go (99%) rename app/desmos/cmd/{ => sign}/sign_test.go (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 837ffcb402..abe9a39eb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Renamed `PollData` and `PollAnswer` to `Poll` and `ProvidedAnswer` ([\#536]((https://github.com/desmos-labs/desmos/issues/536))) - Enabled snapshot by default ([\#529](https://github.com/desmos-labs/desmos/pull/529)) - Improved the performance of profile validation checks ([\#548](https://github.com/desmos-labs/desmos/pull/548)) +- Added `create-chain-link-json` command ([\#572](https://github.com/desmos-labs/desmos/pull/572)) ## Version 0.17.7 diff --git a/app/desmos/cmd/chainlink/create_json.go b/app/desmos/cmd/chainlink/create_json.go new file mode 100644 index 0000000000..6558a8e2d1 --- /dev/null +++ b/app/desmos/cmd/chainlink/create_json.go @@ -0,0 +1,101 @@ +package chainlink + +import ( + "encoding/hex" + "fmt" + "io/ioutil" + + chainlinktypes "github.com/desmos-labs/desmos/app/desmos/cmd/chainlink/types" + + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/spf13/cobra" + + "github.com/desmos-labs/desmos/app" + profilescliutils "github.com/desmos-labs/desmos/x/profiles/client/utils" + profilestypes "github.com/desmos-labs/desmos/x/profiles/types" +) + +// GetCreateChainLinkJSON returns the command allowing to generate the chain link JSON +// file that is required by the link-chain command +func GetCreateChainLinkJSON() *cobra.Command { + return &cobra.Command{ + Use: "create-chain-link-json", + Short: "Start an interactive prompt to create a new chain link JSON object", + Long: `Start an interactive prompt to create a new chain link JSON object that can be used to later link your Desmos profile to another chain. +Once you have built the JSON object using this command, you can then run the following command to complete the linkage: + +desmos tx profiles link-chain [/path/to/json/file.json] + +Note that this command will ask you the mnemonic that should be used to generate the private key of the address you want to link. +The mnemonic is only used temporarily and never stored anywhere.`, + RunE: func(cmd *cobra.Command, args []string) error { + // Build the chain link reference getter + getter := chainlinktypes.NewChainLinkReferencePrompt() + + // Get the data + mnemonic, err := getter.GetMnemonic() + if err != nil { + return err + } + + chain, err := getter.GetChain() + if err != nil { + return err + } + + filename, err := getter.GetFilename() + if err != nil { + return err + } + + // Build che chain link JSON + chainLinkJSON, err := generateChainLinkJSON(mnemonic, chain) + if err != nil { + return err + } + + cdc, _ := app.MakeCodecs() + bz, err := cdc.MarshalJSON(&chainLinkJSON) + if err != nil { + return err + } + + if filename != "" { + if err := ioutil.WriteFile(filename, bz, 0600); err != nil { + return err + } + } + + cmd.Println(fmt.Sprintf("Chain link JSON file stored at %s", filename)) + + return nil + }, + } +} + +// generateChainLinkJSON returns build a new ChainLinkJSON intance using the provided mnemonic and chain configuration +func generateChainLinkJSON(mnemonic string, chain chainlinktypes.Chain) (profilescliutils.ChainLinkJSON, error) { + // Create an in-memory keybase for signing + keyBase := keyring.NewInMemory() + keyName := "chainlink" + _, err := keyBase.NewAccount(keyName, mnemonic, "", chain.DerivationPath, hd.Secp256k1) + if err != nil { + return profilescliutils.ChainLinkJSON{}, err + } + + // Generate the proof signing it with the key + key, _ := keyBase.Key(keyName) + addr, _ := sdk.Bech32ifyAddressBytes(chain.Prefix, key.GetAddress()) + sig, pubkey, err := keyBase.Sign(keyName, []byte(addr)) + if err != nil { + return profilescliutils.ChainLinkJSON{}, err + } + + return profilescliutils.NewChainLinkJSON( + profilestypes.NewBech32Address(addr, chain.Prefix), + profilestypes.NewProof(pubkey, hex.EncodeToString(sig), addr), + profilestypes.NewChainConfig(chain.Name), + ), nil +} diff --git a/app/desmos/cmd/chainlink/create_json_test.go b/app/desmos/cmd/chainlink/create_json_test.go new file mode 100644 index 0000000000..c091ba74e4 --- /dev/null +++ b/app/desmos/cmd/chainlink/create_json_test.go @@ -0,0 +1,83 @@ +package chainlink_test + +import ( + "os" + "testing" + + cmd "github.com/desmos-labs/desmos/app/desmos/cmd/chainlink" + "github.com/desmos-labs/desmos/app/desmos/cmd/chainlink/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/desmos-labs/desmos/app" + profilescliutils "github.com/desmos-labs/desmos/x/profiles/client/utils" + profilestypes "github.com/desmos-labs/desmos/x/profiles/types" +) + +// MockGetter represents a mock implementation of ChainLinkReferenceGetter +type MockGetter struct{} + +// GetMnemonic implements ChainLinkReferenceGetter +func (mock MockGetter) GetMnemonic() (string, error) { + return "clip toilet stairs jaguar baby over mosquito capital speed mule adjust eye print voyage verify smart open crack imitate auto gauge museum planet rebel", nil +} + +// GetChain implements ChainLinkReferenceGetter +func (mock MockGetter) GetChain() (types.Chain, error) { + return types.NewChain("Cosmos", "cosmos", "cosmos", "m/44'/118'/0'/0/0"), nil +} + +// GetFilename implements ChainLinkReferenceGetter +func (mock MockGetter) GetFilename() (string, error) { + return "", nil +} + +func TestGetCreateChainLinkJSON(t *testing.T) { + cfg := sdk.GetConfig() + app.SetupConfig(cfg) + + output := os.Stdout + clientCtx := client.Context{}. + WithOutput(output) + + out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd.GetCreateChainLinkJSON(MockGetter{}), []string{}) + require.NoError(t, err) + + cdc, _ := app.MakeCodecs() + var data profilescliutils.ChainLinkJSON + err = cdc.UnmarshalJSON(out.Bytes(), &data) + require.NoError(t, err) + + // create a account to inmemory keybase + keyBase := keyring.NewInMemory() + keyName := "chainlink" + _, err = keyBase.NewAccount( + "chainlink", + "clip toilet stairs jaguar baby over mosquito capital speed mule adjust eye print voyage verify smart open crack imitate auto gauge museum planet rebel", + "", + "m/44'/118'/0'/0/0", + hd.Secp256k1, + ) + require.NoError(t, err) + + // get key from keybase + key, err := keyBase.Key(keyName) + require.NoError(t, err) + + expected := profilescliutils.NewChainLinkJSON( + profilestypes.NewBech32Address("cosmos13j7p6faa9jr8ty6lvqv0prldprr6m5xenmafnt", "cosmos"), + profilestypes.NewProof( + key.GetPubKey(), + "c3bd014b2178d63d94b9c28e628bfcf56736de28f352841b0bb27d6fff2968d62c13a10aeddd1ebfe3b13f3f8e61f79a2c63ae6ff5cb78cb0d64e6b0a70fae57", + "cosmos13j7p6faa9jr8ty6lvqv0prldprr6m5xenmafnt"), + profilestypes.NewChainConfig("cosmos"), + ) + + require.Equal(t, expected, data) + +} diff --git a/app/desmos/cmd/chainlink/types/chain.go b/app/desmos/cmd/chainlink/types/chain.go new file mode 100644 index 0000000000..460436882c --- /dev/null +++ b/app/desmos/cmd/chainlink/types/chain.go @@ -0,0 +1,19 @@ +package types + +// Chain contains the data of a single chain +type Chain struct { + ID string + Name string + Prefix string + DerivationPath string +} + +// NewChain returns a new Chain instance +func NewChain(id, name, prefix, derivationPath string) Chain { + return Chain{ + id, + name, + prefix, + derivationPath, + } +} diff --git a/app/desmos/cmd/chainlink/types/config.go b/app/desmos/cmd/chainlink/types/config.go new file mode 100644 index 0000000000..5e49f85f1a --- /dev/null +++ b/app/desmos/cmd/chainlink/types/config.go @@ -0,0 +1,19 @@ +package types + +// Config contains the data of the configuration +type Config struct { + Chains []Chain +} + +// DefaultConfig returns the default config instance of the configuration +func DefaultConfig() Config { + return Config{ + Chains: []Chain{ + NewChain("Desmos", "desmos", "desmos", "m/44'/852'/0'/0/0"), + NewChain("Cosmos", "cosmos", "cosmos", "m/44'/118'/0'/0/0"), + NewChain("Akash", "akash", "akash", "m/44'/118'/0'/0/0"), + NewChain("Osmosis", "osmosis", "osmo", "m/44'/118'/0'/0/0"), + NewChain("Other", "", "", ""), + }, + } +} diff --git a/app/desmos/cmd/chainlink/types/getter.go b/app/desmos/cmd/chainlink/types/getter.go new file mode 100644 index 0000000000..6c4e71f906 --- /dev/null +++ b/app/desmos/cmd/chainlink/types/getter.go @@ -0,0 +1,194 @@ +package types + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/cosmos/go-bip39" + + "github.com/manifoldco/promptui" +) + +// ChainLinkReferenceGetter allows to get all the data needed to generate a ChainLinkJSON instance +type ChainLinkReferenceGetter interface { + // GetMnemonic returns the mnemonic + GetMnemonic() (string, error) + + // GetChain returns Chain instance + GetChain() (Chain, error) + + // GetFilename returns filename to save + GetFilename() (string, error) +} + +// ChainLinkReferencePrompt is a ChainLinkReferenceGetter implemented with an interactive prompt +type ChainLinkReferencePrompt struct { + ChainLinkReferenceGetter + cfg Config +} + +// NewChainLinkReferencePrompt returns an instance implementing ChainLinkReferencePrompt +func NewChainLinkReferencePrompt() *ChainLinkReferencePrompt { + return &ChainLinkReferencePrompt{ + cfg: DefaultConfig(), + } +} + +// GetMnemonic implements ChainLinkReferenceGetter +func (cp ChainLinkReferencePrompt) GetMnemonic() (string, error) { + mnemonic, err := cp.getMnemonic() + if err != nil { + return "", err + } + return mnemonic, nil +} + +// GetChain implements ChainLinkReferenceGetter +func (cp ChainLinkReferencePrompt) GetChain() (Chain, error) { + chain, err := cp.selectChain() + if err != nil { + return Chain{}, err + } + + if chain.ID == "Other" { + newChain, err := cp.getCustomChain(chain) + if err != nil { + return Chain{}, err + } + chain = newChain + } + + return chain, nil +} + +// GetFilename implements ChainLinkReferenceGetter +func (cp ChainLinkReferencePrompt) GetFilename() (string, error) { + filename, err := cp.getFilename() + if err != nil { + return "", err + } + return filename, nil +} + +// getMnemonic asks the user the mnemonic and then returns it +func (cp ChainLinkReferencePrompt) getMnemonic() (string, error) { + prompt := promptui.Prompt{ + Label: "Please enter the mnemonic that should be used to generate the address you want to link", + HideEntered: true, + Validate: func(s string) error { + if strings.TrimSpace(s) == "" { + return fmt.Errorf("mnemonic cannot be empty or blank") + } else if _, err := bip39.MnemonicToByteArray(s); err != nil { + return fmt.Errorf("invalid mnemonic") + } + return nil + }, + } + return prompt.Run() +} + +// selectChain asks the user to select a predefined Chain instance, and returns it +func (cp ChainLinkReferencePrompt) selectChain() (Chain, error) { + cfg := cp.cfg + prompt := promptui.Select{ + Label: "Select a target chain", + Items: cfg.Chains, + Templates: &promptui.SelectTemplates{ + Active: "\U00002713 {{ .ID | cyan }}", + Inactive: " {{ .ID | cyan }}", + Selected: "Module: \U00002713 {{ .ID | cyan }}", + }, + } + + index, _, err := prompt.Run() + if err != nil { + return Chain{}, err + } + + return cfg.Chains[index], nil +} + +// getCustomChain asks the user to input the data to build a custom Chain instance, and then returns it +func (cp ChainLinkReferencePrompt) getCustomChain(chain Chain) (Chain, error) { + chainName, err := cp.getChainName() + if err != nil { + return Chain{}, err + } + + prefix, err := cp.getBech32Prefix() + if err != nil { + return Chain{}, err + } + + derivationPath, err := cp.getDerivationPath() + if err != nil { + return Chain{}, err + } + + chain.Name = chainName + chain.Prefix = prefix + chain.DerivationPath = derivationPath + + return chain, nil +} + +// getChainName asks the user to input a chain name, and returns it +func (cp ChainLinkReferencePrompt) getChainName() (string, error) { + prompt := promptui.Prompt{ + Label: "Please input the name of the chain you want to link with", + Validate: func(s string) error { + if strings.TrimSpace(s) == "" { + return fmt.Errorf("chain name cannot be empty or blank") + } + if strings.ToLower(s) != s { + return fmt.Errorf("chain name should be lowercase") + } + return nil + }, + } + return prompt.Run() +} + +// getBech32Prefix asks the user to input the Bech32 prefix of the address, and then returns it +func (cp ChainLinkReferencePrompt) getBech32Prefix() (string, error) { + prompt := promptui.Prompt{ + Label: "Please input the Bech32 account address prefix used inside the the chain", + Validate: func(s string) error { + if strings.TrimSpace(s) == "" { + return fmt.Errorf("bech32 prefix cannot be empty or blank") + } + return nil + }, + } + return prompt.Run() +} + +// getDerivationPath asks the user to input the derivation path of the account, and then returns it +func (cp ChainLinkReferencePrompt) getDerivationPath() (string, error) { + prompt := promptui.Prompt{ + Label: "Please input the derivation path used by the chain to generate the accounts", + Default: "m/44'/118'/0'/0/0", + Validate: func(s string) error { + if strings.TrimSpace(s) == "" { + return fmt.Errorf("derivation path cannot be empty or blank") + } + return nil + }, + } + return prompt.Run() +} + +// getFilename asks the user to input the filename where to store the chain link, and then returns it +func (cp ChainLinkReferencePrompt) getFilename() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + prompt := promptui.Prompt{ + Label: "Please insert where the chain link JSON object should be stored (fully qualified path)", + Default: path.Join(wd, "data.json"), + } + return prompt.Run() +} diff --git a/app/desmos/cmd/root.go b/app/desmos/cmd/root.go index 82a21af1bc..cc7e951594 100644 --- a/app/desmos/cmd/root.go +++ b/app/desmos/cmd/root.go @@ -5,16 +5,19 @@ import ( "os" "path/filepath" + "github.com/desmos-labs/desmos/app/desmos/cmd/chainlink" + "github.com/desmos-labs/desmos/app/desmos/cmd/sign" + config "github.com/cosmos/cosmos-sdk/client/config" "github.com/cosmos/cosmos-sdk/x/crisis" - "github.com/desmos-labs/desmos/app" - "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/simapp/params" "github.com/cosmos/cosmos-sdk/snapshots" + "github.com/desmos-labs/desmos/app" + "github.com/spf13/cast" "github.com/spf13/cobra" tmcli "github.com/tendermint/tendermint/libs/cli" @@ -34,7 +37,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" authclient "github.com/cosmos/cosmos-sdk/x/auth/client" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" - "github.com/cosmos/cosmos-sdk/x/auth/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" cosmosgenutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" @@ -56,7 +59,7 @@ func NewRootCmd() (*cobra.Command, params.EncodingConfig) { WithTxConfig(encodingConfig.TxConfig). WithLegacyAmino(encodingConfig.Amino). WithInput(os.Stdin). - WithAccountRetriever(types.AccountRetriever{}). + WithAccountRetriever(authtypes.AccountRetriever{}). WithBroadcastMode(flags.BroadcastBlock). WithHomeDir(app.DefaultNodeHome). WithViper("DESMOS") @@ -109,7 +112,8 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { rpc.StatusCommand(), queryCommand(), txCommand(), - GetSignCmd(), + sign.GetSignCmd(), + chainlink.GetCreateChainLinkJSON(), keys.Commands(app.DefaultNodeHome), ) } diff --git a/app/desmos/cmd/sign.go b/app/desmos/cmd/sign/sign.go similarity index 99% rename from app/desmos/cmd/sign.go rename to app/desmos/cmd/sign/sign.go index 5695d89393..2b7113509e 100644 --- a/app/desmos/cmd/sign.go +++ b/app/desmos/cmd/sign/sign.go @@ -1,4 +1,4 @@ -package cmd +package sign import ( "encoding/hex" diff --git a/app/desmos/cmd/sign_test.go b/app/desmos/cmd/sign/sign_test.go similarity index 95% rename from app/desmos/cmd/sign_test.go rename to app/desmos/cmd/sign/sign_test.go index 665a0e0b95..388a685adc 100644 --- a/app/desmos/cmd/sign_test.go +++ b/app/desmos/cmd/sign/sign_test.go @@ -1,4 +1,4 @@ -package cmd_test +package sign_test import ( "encoding/json" @@ -6,6 +6,8 @@ import ( "os" "testing" + cmd "github.com/desmos-labs/desmos/app/desmos/cmd/sign" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/crypto/hd" @@ -15,13 +17,11 @@ import ( "github.com/stretchr/testify/require" "github.com/desmos-labs/desmos/app" - cmd "github.com/desmos-labs/desmos/app/desmos/cmd" ) func TestGetSignCmd(t *testing.T) { cfg := sdk.GetConfig() app.SetupConfig(cfg) - cfg.Seal() keyBase := keyring.NewInMemory() algo := hd.Secp256k1 diff --git a/go.mod b/go.mod index f411c9b40b..8c042b1aeb 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/golang/protobuf v1.5.2 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 + github.com/manifoldco/promptui v0.8.0 github.com/mr-tron/base58 v1.2.0 github.com/rakyll/statik v0.1.7 github.com/regen-network/cosmos-proto v0.3.1 diff --git a/go.sum b/go.sum index 25959b56f9..2fcbb1f071 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,12 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= @@ -325,6 +331,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM= @@ -345,10 +353,15 @@ github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOS github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= +github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=