diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9236e281..4845bdaf 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -15,6 +15,13 @@ jobs: with: go-version-file: "go.mod" check-latest: true + - name: Install pass helper + run: sudo apt-get update && sudo apt-get install -y pass + - name: Generate GPG key + run: " + echo \"%no-protection\nKey-Type: 1\nKey-Length: 4096\nSubkey-Type: 1\nSubkey-Length: 4096\nName-Comment: keyring_test\nExpire-Date: 0\" > genkey && gpg --gen-key --batch genkey" + - name: Setup OS keystore + run: pass init keyring_test - name: Run test and calculate coverage run: make coverage - name: Upload coverage to Codecov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6641de94..ac692ca7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,8 @@ exclude: | (?x)^( chain/.*| exchange/.*| - proto/.* + proto/.*| + client/keyring/testdata/.* )$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/client/keyring/README.md b/client/keyring/README.md new file mode 100644 index 00000000..824e6402 --- /dev/null +++ b/client/keyring/README.md @@ -0,0 +1,101 @@ +# Injective Chain Keyring Helper + +Creates a new keyring from a variety of options. See `ConfigOpt` and related options. This keyring helper allows initializing the Cosmos SDK keyring used for signing transactions. + +It allows flexibly defining a static configuration of keys, supports multiple pre-defined keys in the same keyring, and allows loading keys from a file, deriving from mnemonic, or reading plain private key bytes from a HEX string. Extremely useful for testing and local development, but also robust for production use cases. + +## Usage + +```go +NewCosmosKeyring(cdc codec.Codec, opts ...ConfigOpt) (sdk.AccAddress, cosmkeyring.Keyring, error) +``` + +**ConfigOpts:** + +These options are global on the keyring level. + +* `WithKeyringDir` option sets keyring path in the filesystem, useful when keyring backend is `file`. +* `WithKeyringAppName` option sets keyring application name (defaults to `injectived`) +* `WithKeyringBackend` sets the keyring backend. Expected values: `test`, `file`, `os`. +* `WithUseLedger` sets the option to use hardware wallet, if available on the system. + +These options allow adding keys to the keyring during initialization. + +* `WithKey` adds a single key to the keyring, without having alias name. +* `WithNamedKey` addes a single key to the keyring, with a name. +* `WithDefaultKey` sets a default key reference to use for signing (by name). + +**KeyConfigOpts:** + +These options are set per key. + +* `WithKeyFrom` sets the key name to use for signing. Must exist in the provided keyring. +* `WithKeyPassphrase` sets the passphrase for keyring files. The package will fallback to `os.Stdin` if this option was not provided, but passphrase is required. +* `WithPrivKeyHex` allows specifying a private key as plain-text hex. Insecure option, use for testing only. The package will create a virtual keyring holding that key, to meet all the interfaces. +* `WithMnemonic` allows specifying a mnemonic phrase as plain-text hex. Insecure option, use for testing only. The package will create a virtual keyring to derive the keys and meet all the interfaces. + +## Examples + +Initialize an in-memory keyring with a private key hex: + +```go +NewCosmosKeyring( + cdc, + WithKey( + WithPrivKeyHex("e6888cb164d52e4880e08a8a5dbe69cd62f67fde3d5906f2c5c951be553b2267"), + WithKeyFrom("sender"), + ), +) +``` + +Initialize an in-memory keyring with a mnemonic phrase: + +```go +NewCosmosKeyring( + s.cdc, + WithKey( + WithMnemonic("real simple naive ....... love"), + WithKeyFrom("sender"), + ), +) +``` + +Real world use case of keyring initialization from CLI flags, with a single named key set as default: + +```go +NewCosmosKeyring( + cdc, + WithKeyringDir(*keyringDir), + WithKeyringAppName(*keyringAppName), + WithKeyringBackend(Backend(*keyringBackend)), + WithNamedKey( + "dispatcher", + WithKeyFrom(*dispatcherKeyFrom), + WithKeyPassphrase(*dispatcherKeyPassphrase), + WithPrivKeyHex(*dispatcherKeyPrivateHex), + WithMnemonic(*dispatcherKeyMnemonic), + ), + WithDefaultKey( + "dispatcher", + ), +) +``` + +## Testing + +```bash +go test -v -cover + +PASS +coverage: 83.1% of statements +``` + +## Generating a Test Fixture + +```bash +> cd testdata + +> injectived keys --keyring-dir `pwd` --keyring-backend file add test +``` + +Passphrase should be `test12345678` for this fixture to work. diff --git a/client/keyring/errors.go b/client/keyring/errors.go new file mode 100644 index 00000000..bd591844 --- /dev/null +++ b/client/keyring/errors.go @@ -0,0 +1,20 @@ +package keyring + +import "github.com/pkg/errors" + +var ( + ErrCosmosKeyringCreationFailed = errors.New("cosmos keyring creation failed") + ErrCosmosKeyringImportFailed = errors.New("cosmos keyring unable to import key") + ErrDeriveFailed = errors.New("key derivation failed") + ErrFailedToApplyConfigOption = errors.New("failed to apply config option") + ErrFailedToApplyKeyConfigOption = errors.New("failed to apply a key config option") + ErrFilepathIncorrect = errors.New("incorrect filepath") + ErrHexFormatError = errors.New("hex format error") + ErrIncompatibleOptionsProvided = errors.New("incompatible keyring options provided") + ErrInsufficientKeyDetails = errors.New("insufficient cosmos key details provided") + ErrKeyIncompatible = errors.New("provided key is incompatible with requested config") + ErrKeyRecordNotFound = errors.New("key record not found") + ErrPrivkeyConflict = errors.New("privkey conflict") + ErrUnexpectedAddress = errors.New("unexpected address") + ErrMultipleKeysWithDifferentSecurity = errors.New("key security is different: cannot mix keyring with privkeys") +) diff --git a/client/keyring/key_config.go b/client/keyring/key_config.go new file mode 100644 index 00000000..51bb7b45 --- /dev/null +++ b/client/keyring/key_config.go @@ -0,0 +1,69 @@ +package keyring + +import ( + bip39 "github.com/cosmos/go-bip39" + "github.com/pkg/errors" +) + +type cosmosKeyConfig struct { + Name string + KeyFrom string + KeyPassphrase string + PrivKeyHex string + Mnemonic string +} + +// KeyConfigOpt defines a known cosmos keyring key option. +type KeyConfigOpt func(c *cosmosKeyConfig) error + +// WithKeyFrom sets the key name to use for signing. Must exist in the provided keyring. +func WithKeyFrom(v string) KeyConfigOpt { + return func(c *cosmosKeyConfig) error { + if v != "" { + c.KeyFrom = v + } + + return nil + } +} + +// WithKeyPassphrase sets the passphrase for keyring files. Insecure option, use for testing only. +// The package will fallback to os.Stdin if this option was not provided, but pass is required. +func WithKeyPassphrase(v string) KeyConfigOpt { + return func(c *cosmosKeyConfig) error { + if v != "" { + c.KeyPassphrase = v + } + + return nil + } +} + +// WithPrivKeyHex allows to specify a private key as plaintext hex. Insecure option, use for testing only. +// The package will create a virtual keyring holding that key, to meet all the interfaces. +func WithPrivKeyHex(v string) KeyConfigOpt { + return func(c *cosmosKeyConfig) error { + if v != "" { + c.PrivKeyHex = v + } + + return nil + } +} + +// WithMnemonic allows to specify a mnemonic pharse as plaintext. Insecure option, use for testing only. +// The package will create a virtual keyring to derive the keys and meet all the interfaces. +func WithMnemonic(v string) KeyConfigOpt { + return func(c *cosmosKeyConfig) error { + if v != "" { + if !bip39.IsMnemonicValid(v) { + err := errors.New("provided mnemonic is not a valid BIP39 mnemonic") + return err + } + + c.Mnemonic = v + } + + return nil + } +} diff --git a/client/keyring/keyring.go b/client/keyring/keyring.go new file mode 100644 index 00000000..18fb8473 --- /dev/null +++ b/client/keyring/keyring.go @@ -0,0 +1,497 @@ +package keyring + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "io" + "os" + "path/filepath" + "strings" + + "github.com/InjectiveLabs/sdk-go/chain/crypto/hd" + "github.com/cosmos/cosmos-sdk/codec" + cosmcrypto "github.com/cosmos/cosmos-sdk/crypto" + cosmkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pkg/errors" +) + +var ( + defaultKeyringKeyName = "default" + emptyCosmosAddress = sdk.AccAddress{} +) + +// NewCosmosKeyring creates a new keyring from a variety of options. See ConfigOpt and related options. +func NewCosmosKeyring(cdc codec.Codec, opts ...ConfigOpt) (sdk.AccAddress, cosmkeyring.Keyring, error) { + config := &cosmosKeyringConfig{} + for optIdx, optFn := range opts { + if err := optFn(config); err != nil { + err = errors.Wrapf(ErrFailedToApplyConfigOption, "option #%d: %s", optIdx+1, err.Error()) + return emptyCosmosAddress, nil, err + } + } + + if len(config.Keys) == 0 { + return emptyCosmosAddress, nil, ErrInsufficientKeyDetails + } + + var kb cosmkeyring.Keyring + var realKB cosmkeyring.Keyring + var usingRealKeyring bool + var firstKey *sdk.AccAddress + + for keyIdx, keyConfig := range config.Keys { + switch { + case keyConfig.Mnemonic != "": + if usingRealKeyring { + return emptyCosmosAddress, nil, ErrMultipleKeysWithDifferentSecurity + } else if kb == nil { + kb = cosmkeyring.NewInMemory(cdc, hd.EthSecp256k1Option()) + } + + if config.UseLedger { + err := errors.Wrap(ErrIncompatibleOptionsProvided, "cannot combine ledger and mnemonic options") + return emptyCosmosAddress, nil, err + } + + addr, err := fromMnemonic(kb, keyConfig) + if err != nil { + return addr, kb, err + } + + if keyIdx == 0 { + firstKey = &addr + } + + case keyConfig.PrivKeyHex != "": + if usingRealKeyring { + return emptyCosmosAddress, nil, ErrMultipleKeysWithDifferentSecurity + } else if kb == nil { + kb = cosmkeyring.NewInMemory(cdc, hd.EthSecp256k1Option()) + } + + if config.UseLedger { + err := errors.Wrap(ErrIncompatibleOptionsProvided, "cannot combine ledger and privkey options") + return emptyCosmosAddress, nil, err + } + + addr, err := fromPrivkeyHex(kb, keyConfig) + if err != nil { + return addr, kb, err + } + + if keyIdx == 0 { + firstKey = &addr + } + + case keyConfig.KeyFrom != "": + if kb != nil { + return emptyCosmosAddress, nil, ErrMultipleKeysWithDifferentSecurity + } else { + usingRealKeyring = true + } + + var fromIsAddress bool + + addressFrom, err := sdk.AccAddressFromBech32(keyConfig.KeyFrom) + if err == nil { + fromIsAddress = true + } + + addr, kb, err := fromCosmosKeyring(cdc, config, keyConfig, addressFrom, fromIsAddress) + if err != nil { + return addr, kb, err + } + + realKB = kb + if keyIdx == 0 { + firstKey = &addr + } + + default: + err := errors.Wrapf(ErrInsufficientKeyDetails, "key %d details", keyIdx+1) + return emptyCosmosAddress, nil, err + } + } + + if realKB != nil { + if config.DefaultKey != "" { + defaultKeyAddr, err := findKeyInKeyring(realKB, config, config.DefaultKey) + if err != nil { + return emptyCosmosAddress, nil, err + } + + return defaultKeyAddr, realKB, nil + } + + return *firstKey, realKB, nil + } + + if config.DefaultKey != "" { + defaultKeyAddr, err := findKeyInKeyring(kb, config, config.DefaultKey) + if err != nil { + return emptyCosmosAddress, nil, err + } + + return defaultKeyAddr, kb, nil + } + + return *firstKey, kb, nil +} + +func fromPrivkeyHex( + kb cosmkeyring.Keyring, + keyConfig *cosmosKeyConfig, +) (sdk.AccAddress, error) { + pkBytes, err := hexToBytes(keyConfig.PrivKeyHex) + if err != nil { + err = errors.Wrapf(ErrHexFormatError, "failed to decode cosmos account privkey: %s", err.Error()) + return emptyCosmosAddress, err + } + + cosmosAccPk := hd.EthSecp256k1.Generate()(pkBytes) + addressFromPk := sdk.AccAddress(cosmosAccPk.PubKey().Address().Bytes()) + + keyName := keyConfig.Name + + // check that if cosmos 'From' specified separately, it must match the provided privkey + if keyConfig.KeyFrom != "" { + addressFrom, err := sdk.AccAddressFromBech32(keyConfig.KeyFrom) + + switch { + case err == nil: + if !bytes.Equal(addressFrom.Bytes(), addressFromPk.Bytes()) { + err = errors.Wrapf( + ErrUnexpectedAddress, + "expected account address %s but got %s from the private key", + addressFrom.String(), addressFromPk.String(), + ) + + return emptyCosmosAddress, err + } + + case keyName == "": + // use it as a name then + keyName = keyConfig.KeyFrom + + case keyName != keyConfig.KeyFrom: + err := errors.Errorf( + "key 'from' opt is a name, but doesn't match given key name: %s != %s", + keyConfig.KeyFrom, keyName, + ) + + return emptyCosmosAddress, err + } + } + + if keyName == "" { + keyName = defaultKeyringKeyName + } + + // add a PK into a Keyring + err = addFromPrivKey(kb, keyName, cosmosAccPk) + if err != nil { + err = errors.WithStack(err) + } + + return addressFromPk, err +} + +func fromMnemonic( + kb cosmkeyring.Keyring, + keyConfig *cosmosKeyConfig, +) (sdk.AccAddress, error) { + cfg := sdk.GetConfig() + + pkBytes, err := hd.EthSecp256k1.Derive()( + keyConfig.Mnemonic, + cosmkeyring.DefaultBIP39Passphrase, + cfg.GetFullBIP44Path(), + ) + if err != nil { + err = errors.Wrapf(ErrDeriveFailed, "failed to derive secp256k1 private key: %s", err.Error()) + return emptyCosmosAddress, err + } + + cosmosAccPk := hd.EthSecp256k1.Generate()(pkBytes) + addressFromPk := sdk.AccAddress(cosmosAccPk.PubKey().Address().Bytes()) + + keyName := keyConfig.Name + + // check that if cosmos 'From' specified separately, it must match the derived privkey + if keyConfig.KeyFrom != "" { + addressFrom, err := sdk.AccAddressFromBech32(keyConfig.KeyFrom) + switch { + case err == nil: + if !bytes.Equal(addressFrom.Bytes(), addressFromPk.Bytes()) { + err = errors.Wrapf( + ErrUnexpectedAddress, + "expected account address %s but got %s from the mnemonic at /0", + addressFrom.String(), addressFromPk.String(), + ) + + return emptyCosmosAddress, err + } + case keyName == "": + // use it as a name then + keyName = keyConfig.KeyFrom + case keyName != keyConfig.KeyFrom: + err := errors.Errorf( + "key 'from' opt is a name, but doesn't match given key name: %s != %s", + keyConfig.KeyFrom, keyName, + ) + return emptyCosmosAddress, err + } + } + + // check that if 'PrivKeyHex' specified separately, it must match the derived privkey too + if keyConfig.PrivKeyHex != "" { + if err := checkPrivkeyHexMatchesMnemonic(keyConfig.PrivKeyHex, pkBytes); err != nil { + return emptyCosmosAddress, err + } + } + + if keyName == "" { + keyName = defaultKeyringKeyName + } + + // add a PK into a Keyring + err = addFromPrivKey(kb, keyName, cosmosAccPk) + if err != nil { + err = errors.WithStack(err) + } + + return addressFromPk, err +} + +func checkPrivkeyHexMatchesMnemonic(pkHex string, mnemonicDerivedPkBytes []byte) error { + pkBytesFromHex, err := hexToBytes(pkHex) + if err != nil { + err = errors.Wrapf(ErrHexFormatError, "failed to decode cosmos account privkey: %s", err.Error()) + return err + } + + if !bytes.Equal(mnemonicDerivedPkBytes, pkBytesFromHex) { + err := errors.Wrap( + ErrPrivkeyConflict, + "both mnemonic and privkey hex options provided, but privkey doesn't match mnemonic", + ) + return err + } + + return nil +} + +func fromCosmosKeyring( + cdc codec.Codec, + config *cosmosKeyringConfig, + keyConfig *cosmosKeyConfig, + fromAddress sdk.AccAddress, + fromIsAddress bool, +) (sdk.AccAddress, cosmkeyring.Keyring, error) { + var passReader io.Reader = os.Stdin + if keyConfig.KeyPassphrase != "" { + passReader = newPassReader(keyConfig.KeyPassphrase) + } + + var err error + absoluteKeyringDir := config.KeyringDir + if !filepath.IsAbs(config.KeyringDir) { + absoluteKeyringDir, err = filepath.Abs(config.KeyringDir) + if err != nil { + err = errors.Wrapf(ErrFilepathIncorrect, "failed to get abs path for keyring dir: %s", err.Error()) + return emptyCosmosAddress, nil, err + } + } + + kb, err := cosmkeyring.New( + config.KeyringAppName, + string(config.KeyringBackend), + absoluteKeyringDir, + passReader, + cdc, + hd.EthSecp256k1Option(), + ) + if err != nil { + err = errors.Wrapf(ErrCosmosKeyringCreationFailed, "failed to init cosmos keyring: %s", err.Error()) + return emptyCosmosAddress, nil, err + } + + var keyRecord *cosmkeyring.Record + if fromIsAddress { + keyRecord, err = kb.KeyByAddress(fromAddress) + } else { + keyName := keyConfig.Name + if keyName != "" && keyConfig.KeyFrom != keyName { + err := errors.Errorf( + "key 'from' opt is a name, but doesn't match given key name: %s != %s", + keyConfig.KeyFrom, keyName, + ) + + return emptyCosmosAddress, nil, err + } + + keyRecord, err = kb.Key(keyConfig.KeyFrom) + } + + if err != nil { + err = errors.Wrapf( + ErrKeyRecordNotFound, "couldn't find an entry for the key '%s' in keybase: %s", + keyConfig.KeyFrom, err.Error()) + + return emptyCosmosAddress, nil, err + } + + if err := checkKeyRecord(config, keyRecord); err != nil { + return emptyCosmosAddress, nil, err + } + + addr, err := keyRecord.GetAddress() + if err != nil { + return emptyCosmosAddress, nil, err + } + + return addr, kb, nil +} + +func findKeyInKeyring(kb cosmkeyring.Keyring, config *cosmosKeyringConfig, fromSpec string) (sdk.AccAddress, error) { + var fromIsAddress bool + + addressFrom, err := sdk.AccAddressFromBech32(fromSpec) + if err == nil { + fromIsAddress = true + } + + var keyRecord *cosmkeyring.Record + if fromIsAddress { + keyRecord, err = kb.KeyByAddress(addressFrom) + } else { + keyRecord, err = kb.Key(fromSpec) + } + + if err != nil { + err = errors.Wrapf( + ErrKeyRecordNotFound, "couldn't find an entry for the key '%s' in keybase: %s", + fromSpec, err.Error()) + + return emptyCosmosAddress, err + } + + if err := checkKeyRecord(config, keyRecord); err != nil { + return emptyCosmosAddress, err + } + + addr, err := keyRecord.GetAddress() + if err != nil { + return emptyCosmosAddress, err + } + + return addr, nil +} + +func checkKeyRecord( + config *cosmosKeyringConfig, + keyRecord *cosmkeyring.Record, +) error { + switch keyType := keyRecord.GetType(); keyType { + case cosmkeyring.TypeLocal: + // kb has a key and it's totally usable + return nil + + case cosmkeyring.TypeLedger: + // the kb stores references to ledger keys, so we must explicitly + // check that. kb doesn't know how to scan HD keys - they must be added manually before + if config.UseLedger { + return nil + } + err := errors.Wrapf( + ErrKeyIncompatible, + "'%s' key is a ledger reference, enable ledger option", + keyRecord.Name, + ) + return err + + case cosmkeyring.TypeOffline: + err := errors.Wrapf( + ErrKeyIncompatible, + "'%s' key is an offline key, not supported yet", + keyRecord.Name, + ) + return err + + case cosmkeyring.TypeMulti: + err := errors.Wrapf( + ErrKeyIncompatible, + "'%s' key is an multisig key, not supported yet", + keyRecord.Name, + ) + return err + + default: + err := errors.Wrapf( + ErrKeyIncompatible, + "'%s' key has unsupported type: %s", + keyRecord.Name, keyType, + ) + return err + } +} + +func newPassReader(pass string) io.Reader { + return &passReader{ + pass: pass, + buf: new(bytes.Buffer), + } +} + +type passReader struct { + pass string + buf *bytes.Buffer +} + +var _ io.Reader = &passReader{} + +func (r *passReader) Read(p []byte) (n int, err error) { + n, err = r.buf.Read(p) + if err == io.EOF || n == 0 { + r.buf.WriteString(r.pass + "\n") + + n, err = r.buf.Read(p) + } + + return n, err +} + +// addFromPrivKey adds a PrivKey into temporary in-mem keyring. +// Allows to init Context when the key has been provided in plaintext and parsed. +func addFromPrivKey(kb cosmkeyring.Keyring, name string, privKey cryptotypes.PrivKey) error { + tmpPhrase := randPhrase(64) + armored := cosmcrypto.EncryptArmorPrivKey(privKey, tmpPhrase, privKey.Type()) + err := kb.ImportPrivKey(name, armored, tmpPhrase) + if err != nil { + err = errors.Wrapf(ErrCosmosKeyringImportFailed, "failed to import privkey: %s", err.Error()) + return err + } + + return nil +} + +func hexToBytes(str string) ([]byte, error) { + data, err := hex.DecodeString(strings.TrimPrefix(str, "0x")) + if err != nil { + return nil, err + } + + return data, nil +} + +func randPhrase(size int) string { + buf := make([]byte, size) + if _, err := rand.Read(buf); err != nil { + panic(err) + } + + return string(buf) +} diff --git a/client/keyring/keyring_config.go b/client/keyring/keyring_config.go new file mode 100644 index 00000000..ebf3e74b --- /dev/null +++ b/client/keyring/keyring_config.go @@ -0,0 +1,120 @@ +package keyring + +import ( + "github.com/pkg/errors" +) + +// ConfigOpt defines a known cosmos keyring option. +type ConfigOpt func(c *cosmosKeyringConfig) error + +type cosmosKeyringConfig struct { + KeyringDir string + KeyringAppName string + KeyringBackend Backend + UseLedger bool + + Keys []*cosmosKeyConfig + DefaultKey string +} + +// Backend defines a known keyring backend name. +type Backend string + +const ( + // BackendTest is a testing backend, no passphrases required. + BackendTest Backend = "test" + // BackendFile is a backend where keys are stored as encrypted files. + BackendFile Backend = "file" + // BackendOS is a backend where keys are stored in the OS key chain. Platform specific. + BackendOS Backend = "os" +) + +// WithKeyringDir option sets keyring path in the filesystem, useful when keyring backend is `file`. +func WithKeyringDir(v string) ConfigOpt { + return func(c *cosmosKeyringConfig) error { + if v != "" { + c.KeyringDir = v + } + + return nil + } +} + +// WithKeyringAppName option sets keyring application name (used by Cosmos to separate keyrings). +func WithKeyringAppName(v string) ConfigOpt { + return func(c *cosmosKeyringConfig) error { + if v != "" { + c.KeyringAppName = v + } + + return nil + } +} + +// WithKeyringBackend sets the keyring backend. Expected values: test, file, os. +func WithKeyringBackend(v Backend) ConfigOpt { + return func(c *cosmosKeyringConfig) error { + if len(v) > 0 { + c.KeyringBackend = v + } + + return nil + } +} + +// WithUseLedger sets the option to use hardware wallet, if available on the system. +func WithUseLedger(b bool) ConfigOpt { + return func(c *cosmosKeyringConfig) error { + c.UseLedger = b + + return nil + } +} + +// WithKey adds an unnamed key into the keyring, based on its individual options. +func WithKey(opts ...KeyConfigOpt) ConfigOpt { + return func(c *cosmosKeyringConfig) error { + config := &cosmosKeyConfig{} + + for optIdx, optFn := range opts { + if err := optFn(config); err != nil { + err = errors.Wrapf(ErrFailedToApplyKeyConfigOption, "key option #%d: %s", optIdx+1, err.Error()) + return err + } + } + + c.Keys = append(c.Keys, config) + return nil + } +} + +// WithNamedKey adds a key into the keyring, based on its individual options, with a given name (alias). +func WithNamedKey(name string, opts ...KeyConfigOpt) ConfigOpt { + return func(c *cosmosKeyringConfig) error { + config := &cosmosKeyConfig{ + Name: name, + } + + for optIdx, optFn := range opts { + if err := optFn(config); err != nil { + err = errors.Wrapf(ErrFailedToApplyKeyConfigOption, "key option #%d: %s", optIdx+1, err.Error()) + return err + } + } + + c.Keys = append(c.Keys, config) + return nil + } +} + +// WithDefaultKey specifies the default key (name or address) to be fetched during keyring init. +// This key must exist in specified keys. +func WithDefaultKey(v string) ConfigOpt { + return func(c *cosmosKeyringConfig) error { + if v != "" { + c.DefaultKey = v + } + + return nil + } +} diff --git a/client/keyring/keyring_errors_test.go b/client/keyring/keyring_errors_test.go new file mode 100644 index 00000000..f2f3d52c --- /dev/null +++ b/client/keyring/keyring_errors_test.go @@ -0,0 +1,193 @@ +package keyring + +import ( + "os" + + "github.com/InjectiveLabs/sdk-go/chain/crypto/hd" + cosmkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" +) + +func (s *KeyringTestSuite) TestErrCosmosKeyringCreationFailed() { + requireT := s.Require() + + _, _, err := NewCosmosKeyring( + s.cdc, + WithKeyringBackend("kowabunga"), + WithKey( + WithKeyFrom(testAccAddressBech), + ), + ) + + requireT.ErrorIs(err, ErrCosmosKeyringCreationFailed) +} + +func (s *KeyringTestSuite) TestErrFailedToApplyConfigOption() { + requireT := s.Require() + + _, _, err := NewCosmosKeyring( + s.cdc, + WithKey( + WithMnemonic(`???`), + ), + ) + + requireT.ErrorIs(err, ErrFailedToApplyConfigOption) +} + +func (s *KeyringTestSuite) TestErrHexFormatError() { + requireT := s.Require() + + _, _, err := NewCosmosKeyring( + s.cdc, + WithKey( + WithPrivKeyHex("nothex"), + ), + ) + + requireT.ErrorIs(err, ErrHexFormatError) + + _, _, err = NewCosmosKeyring( + s.cdc, + WithKey( + WithMnemonic(testMnemonic), + WithPrivKeyHex("nothex"), + ), + ) + + requireT.ErrorIs(err, ErrHexFormatError) +} + +func (s *KeyringTestSuite) TestErrIncompatibleOptionsProvided() { + requireT := s.Require() + + _, _, err := NewCosmosKeyring( + s.cdc, + WithUseLedger(true), + WithKey( + WithMnemonic(testMnemonic), + ), + ) + + requireT.ErrorIs(err, ErrIncompatibleOptionsProvided) + + _, _, err = NewCosmosKeyring( + s.cdc, + WithUseLedger(true), + WithKey( + WithPrivKeyHex(testPrivKeyHex), + ), + ) + + requireT.ErrorIs(err, ErrIncompatibleOptionsProvided) +} + +func (s *KeyringTestSuite) TestErrInsufficientKeyDetails() { + requireT := s.Require() + + _, _, err := NewCosmosKeyring(s.cdc) + + requireT.ErrorIs(err, ErrInsufficientKeyDetails) +} + +func (s *KeyringTestSuite) TestErrKeyIncompatible() { + requireT := s.Require() + + addr, kb, err := NewCosmosKeyring( + s.cdc, + WithKey( + WithPrivKeyHex(testPrivKeyHex), + ), + ) + requireT.NoError(err) + + testRecord, err := kb.KeyByAddress(addr) + requireT.NoError(err) + testRecordPubKey, err := testRecord.GetPubKey() + requireT.NoError(err) + + kbDir, err := os.MkdirTemp(os.TempDir(), "keyring-test-kbroot-*") + requireT.NoError(err) + s.T().Cleanup(func() { + _ = os.RemoveAll(kbDir) + }) + + testKeyring, err := cosmkeyring.New( + KeyringAppName, + cosmkeyring.BackendTest, + kbDir, + nil, + s.cdc, + hd.EthSecp256k1Option(), + ) + requireT.NoError(err) + + _, err = testKeyring.SaveOfflineKey("test_pubkey", testRecordPubKey) + requireT.NoError(err) + + _, _, err = NewCosmosKeyring( + s.cdc, + WithKeyringBackend(BackendTest), + WithKeyringDir(kbDir), + WithKeyringAppName(KeyringAppName), + WithKey( + WithKeyFrom("test_pubkey"), + ), + ) + requireT.ErrorIs(err, ErrKeyIncompatible) + + // TODO: add test for unsupported multisig keys +} + +func (s *KeyringTestSuite) TestErrKeyRecordNotFound() { + requireT := s.Require() + + _, _, err := NewCosmosKeyring( + s.cdc, + WithKeyringBackend(BackendFile), + WithKeyringDir("./testdata"), + WithKey( + WithKeyFrom("kowabunga"), + WithKeyPassphrase("test12345678"), + ), + ) + + requireT.ErrorIs(err, ErrKeyRecordNotFound) +} + +func (s *KeyringTestSuite) TestErrPrivkeyConflict() { + requireT := s.Require() + + _, _, err := NewCosmosKeyring( + s.cdc, + WithKey( + WithPrivKeyHex(testOtherPrivKeyHex), + WithMnemonic(testMnemonic), // different mnemonic + ), + ) + + requireT.ErrorIs(err, ErrPrivkeyConflict) +} + +func (s *KeyringTestSuite) TestErrUnexpectedAddress() { + requireT := s.Require() + + _, _, err := NewCosmosKeyring( + s.cdc, + WithKey( + WithPrivKeyHex(testOtherPrivKeyHex), + WithKeyFrom(testAccAddressBech), // will not match privkey above + ), + ) + + requireT.ErrorIs(err, ErrUnexpectedAddress) + + _, _, err = NewCosmosKeyring( + s.cdc, + WithKey( + WithMnemonic(testMnemonic), + WithKeyFrom("inj1xypj9l9sjdaduaafhgx39ru70utnzfuklcpxz9"), // will not match mnemonic above + ), + ) + + requireT.ErrorIs(err, ErrUnexpectedAddress) +} diff --git a/client/keyring/keyring_test.go b/client/keyring/keyring_test.go new file mode 100644 index 00000000..38693b7b --- /dev/null +++ b/client/keyring/keyring_test.go @@ -0,0 +1,304 @@ +package keyring + +import ( + "encoding/hex" + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + cosmcrypto "github.com/cosmos/cosmos-sdk/crypto" + cosmkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + crypto_cdc "github.com/InjectiveLabs/sdk-go/chain/crypto/codec" + "github.com/InjectiveLabs/sdk-go/chain/crypto/hd" + ctypes "github.com/InjectiveLabs/sdk-go/chain/types" + "github.com/InjectiveLabs/sdk-go/client/chain" +) + +const KeyringAppName = "keyring_test" + +type KeyringTestSuite struct { + suite.Suite + + cdc codec.Codec +} + +func TestKeyringTestSuite(t *testing.T) { + suite.Run(t, new(KeyringTestSuite)) +} + +func getCryptoCodec() *codec.ProtoCodec { + registry := chain.NewInterfaceRegistry() + crypto_cdc.RegisterInterfaces(registry) + return codec.NewProtoCodec(registry) +} + +func (s *KeyringTestSuite) SetupTest() { + config := sdk.GetConfig() + ctypes.SetBech32Prefixes(config) + ctypes.SetBip44CoinType(config) + + s.cdc = getCryptoCodec() +} + +func (s *KeyringTestSuite) TestKeyFromPrivkey() { + requireT := s.Require() + + accAddr, kb, err := NewCosmosKeyring( + s.cdc, + WithKey( + WithPrivKeyHex(testPrivKeyHex), + WithKeyFrom(testAccAddressBech), // must match the privkey above + ), + ) + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) + + record, err := kb.KeyByAddress(accAddr) + requireT.NoError(err) + requireT.Equal(cosmkeyring.TypeLocal, record.GetType()) + requireT.Equal(expectedPubKeyType, record.PubKey.TypeUrl) + recordPubKey, err := record.GetPubKey() + requireT.NoError(err) + + logPrivKey(s.T(), kb, accAddr) + + res, pubkey, err := kb.SignByAddress(accAddr, []byte("test"), signing.SignMode_SIGN_MODE_DIRECT) + requireT.NoError(err) + requireT.EqualValues(recordPubKey, pubkey) + requireT.Equal(testSig, res) +} + +func (s *KeyringTestSuite) TestKeyFromMnemonic() { + requireT := s.Require() + + accAddr, kb, err := NewCosmosKeyring( + s.cdc, + WithKey( + WithMnemonic(testMnemonic), + WithPrivKeyHex(testPrivKeyHex), // must match mnemonic above + WithKeyFrom(testAccAddressBech), // must match mnemonic above + ), + ) + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) + + record, err := kb.KeyByAddress(accAddr) + requireT.NoError(err) + requireT.Equal(cosmkeyring.TypeLocal, record.GetType()) + requireT.Equal(expectedPubKeyType, record.PubKey.TypeUrl) + recordPubKey, err := record.GetPubKey() + requireT.NoError(err) + + logPrivKey(s.T(), kb, accAddr) + + res, pubkey, err := kb.SignByAddress(accAddr, []byte("test"), signing.SignMode_SIGN_MODE_DIRECT) + requireT.NoError(err) + requireT.Equal(recordPubKey, pubkey) + requireT.Equal(testSig, res) +} + +func (s *KeyringTestSuite) TestKeyringFile() { + requireT := s.Require() + + accAddr, _, err := NewCosmosKeyring( + s.cdc, + WithKeyringBackend(BackendFile), + WithKeyringDir("./testdata"), + WithKey( + WithKeyFrom("test"), + WithKeyPassphrase("test12345678"), + ), + ) + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) + + accAddr, kb, err := NewCosmosKeyring( + s.cdc, + WithKeyringBackend(BackendFile), + WithKeyringDir("./testdata"), + WithKey( + WithKeyFrom(testAccAddressBech), + WithKeyPassphrase("test12345678"), + ), + ) + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) + + record, err := kb.KeyByAddress(accAddr) + requireT.NoError(err) + requireT.Equal(cosmkeyring.TypeLocal, record.GetType()) + requireT.Equal(expectedPubKeyType, record.PubKey.TypeUrl) + requireT.Equal("test", record.Name) + recordPubKey, err := record.GetPubKey() + requireT.NoError(err) + + logPrivKey(s.T(), kb, accAddr) + + res, pubkey, err := kb.SignByAddress(accAddr, []byte("test"), signing.SignMode_SIGN_MODE_DIRECT) + requireT.NoError(err) + requireT.Equal(recordPubKey, pubkey) + requireT.Equal(testSig, res) +} + +func (s *KeyringTestSuite) TestKeyringOsWithAppName() { + if testing.Short() { + s.T().Skip("skipping testing in short mode") + return + } + + requireT := require.New(s.T()) + + osKeyring, err := cosmkeyring.New( + KeyringAppName, + cosmkeyring.BackendOS, + "", + nil, + s.cdc, + hd.EthSecp256k1Option(), + ) + requireT.NoError(err) + + var accRecord *cosmkeyring.Record + if accRecord, err = osKeyring.Key("test"); err != nil { + accRecord, err = osKeyring.NewAccount( + "test", + testMnemonic, + cosmkeyring.DefaultBIP39Passphrase, + sdk.GetConfig().GetFullBIP44Path(), + hd.EthSecp256k1, + ) + + requireT.NoError(err) + + accAddr, err := accRecord.GetAddress() + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) + } + + s.T().Cleanup(func() { + // cleanup + addr, err := accRecord.GetAddress() + if err == nil { + _ = osKeyring.DeleteByAddress(addr) + } + }) + + accAddr, kb, err := NewCosmosKeyring( + s.cdc, + WithKeyringBackend(BackendOS), + WithKeyringAppName(KeyringAppName), + WithKey( + WithKeyFrom("test"), + ), + ) + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) + + record, err := kb.KeyByAddress(accAddr) + requireT.NoError(err) + requireT.Equal(cosmkeyring.TypeLocal, record.GetType()) + requireT.Equal(expectedPubKeyType, record.PubKey.TypeUrl) + recordPubKey, err := record.GetPubKey() + requireT.NoError(err) + + requireT.Equal("test", record.Name) + + res, pubkey, err := kb.SignByAddress(accAddr, []byte("test"), signing.SignMode_SIGN_MODE_DIRECT) + requireT.NoError(err) + requireT.Equal(recordPubKey, pubkey) + requireT.Equal(testSig, res) +} + +func (s *KeyringTestSuite) TestUseFromAsName() { + requireT := s.Require() + + accAddr, _, err := NewCosmosKeyring( + s.cdc, + WithKey( + WithPrivKeyHex(testPrivKeyHex), + WithKeyFrom("kowabunga"), + ), + WithDefaultKey("kowabunga"), + ) + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) + + accAddr, _, err = NewCosmosKeyring( + s.cdc, + WithKey( + WithMnemonic(testMnemonic), + WithKeyFrom("kowabunga"), + ), + WithDefaultKey("kowabunga"), + ) + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) +} + +func (s *KeyringTestSuite) TestNamedKeys() { + requireT := s.Require() + + accAddr, kb, err := NewCosmosKeyring( + s.cdc, + WithNamedKey( + "bad", + WithPrivKeyHex(testOtherPrivKeyHex), + ), + + WithNamedKey( + "good", + WithPrivKeyHex(testPrivKeyHex), + ), + + WithDefaultKey("good"), + ) + + requireT.NoError(err) + requireT.Equal(testAccAddressBech, accAddr.String()) + + record, err := kb.KeyByAddress(accAddr) + requireT.NoError(err) + requireT.Equal(cosmkeyring.TypeLocal, record.GetType()) + requireT.Equal(expectedPubKeyType, record.PubKey.TypeUrl) + recordPubKey, err := record.GetPubKey() + requireT.NoError(err) + + logPrivKey(s.T(), kb, accAddr) + + res, pubkey, err := kb.SignByAddress(accAddr, []byte("test"), signing.SignMode_SIGN_MODE_DIRECT) + requireT.NoError(err) + requireT.EqualValues(recordPubKey, pubkey) + requireT.Equal(testSig, res) +} + +const expectedPubKeyType = "/injective.crypto.v1beta1.ethsecp256k1.PubKey" + +const testAccAddressBech = "inj1ycc302kea06htx5zw2kj4eyk3hgj63sz206fq0" + +//nolint:lll // mnemonic fixture +const testMnemonic = `real simple naive tissue alcohol bar short joy maze shoe reason item tray attitude panda century pulse skirt original autumn sea shop exhaust love` + +var testPrivKeyHex = "e6888cb164d52e4880e08a8a5dbe69cd62f67fde3d5906f2c5c951be553b2267" +var testOtherPrivKeyHex = "ef3bc8bc1e1bae12268e0192787673a4137af840bfcbd1aa4c535bbd95fe6837" + +var testSig = []byte{ + 0xf9, 0x04, 0x3e, 0x81, 0x83, 0xb2, 0x73, 0xf6, + 0xdd, 0xf7, 0xd6, 0x91, 0x6f, 0xb5, 0x63, 0xf4, + 0x8a, 0xa2, 0x4a, 0x51, 0x63, 0xe1, 0x04, 0x18, + 0xd2, 0xe6, 0xed, 0x9e, 0xda, 0x52, 0x2f, 0x0a, + 0x69, 0x74, 0x04, 0x73, 0x7b, 0x9a, 0xf1, 0xc8, + 0xdf, 0xe7, 0xf3, 0x4a, 0x48, 0xe6, 0x5f, 0xc0, + 0x69, 0x5e, 0x6e, 0x03, 0x9e, 0x6e, 0x5f, 0x31, + 0xa6, 0x40, 0x19, 0x1b, 0x76, 0x07, 0xd9, 0x65, + 0x00, +} + +func logPrivKey(t *testing.T, kb cosmkeyring.Keyring, accAddr sdk.AccAddress) { + armor, _ := kb.ExportPrivKeyArmorByAddress(accAddr, "") + privKey, _, _ := cosmcrypto.UnarmorDecryptPrivKey(armor, "") + t.Log("[PRIV]", hex.EncodeToString(privKey.Bytes())) +} diff --git a/client/keyring/testdata/keyring-file/263117aad9ebf5759a8272ad2ae4968dd12d4602.address b/client/keyring/testdata/keyring-file/263117aad9ebf5759a8272ad2ae4968dd12d4602.address new file mode 100644 index 00000000..4730fa37 --- /dev/null +++ b/client/keyring/testdata/keyring-file/263117aad9ebf5759a8272ad2ae4968dd12d4602.address @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wOS0wNyAxMzo0OTowNi42NDcwMjMgKzAyMDAgQ0VTVCBtPSs4MS42NDQ1NDE5MTgiLCJlbmMiOiJBMjU2R0NNIiwicDJjIjo4MTkyLCJwMnMiOiJyY1pRdHMtbzJFaDFGZHhCIn0.-BfYEQoZEiTDwc9fsuLawiEIE_P8Q8KAKhbk3aU0b1-YQv8Brjsihg.bwXK-xKtobJd3mWC.c5rNN9FoqUpnrIjwqU3xPqcgCUgbCF8GgAUibcQfmyYk1MIvbM7aSx1y6ngO0UCRLCZdPhJgxxfBAbPZrbFe7DL4XKn5RfdbPO0a43pN0CHiHu3z86YlPwTS2ADQSta1Zup_ek2boS39GOAgXgC1kYIiox8b1aM_zOvx7M1ASdhnRqMoGu-kYth0FQoocbYRlTF9WMGR40iW-xfSFSYxvoORZDYvfgy8_hIjAYw7bfFkHQ.WWP2VXAeTBZ5L30NTqNbfg \ No newline at end of file diff --git a/client/keyring/testdata/keyring-file/310322fcb0937ade77a9ba0d128f9e7f17312796.address b/client/keyring/testdata/keyring-file/310322fcb0937ade77a9ba0d128f9e7f17312796.address new file mode 100644 index 00000000..7b5e5e2c --- /dev/null +++ b/client/keyring/testdata/keyring-file/310322fcb0937ade77a9ba0d128f9e7f17312796.address @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wOS0wNyAxMzo1MzozMi41MjczNjIgKzAyMDAgQ0VTVCBtPSswLjU0NDc3NTc1MSIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6IjhGTDlhUXNlSzVZV2RlblcifQ.cc0WF0wygByoLnNwnrAeFMqRbzFduFelXwQlKVbnYg7Y7sQoVCtVzQ.qC1rIh3zSt9Lfc-V.6MPAr7OMxRwq91SM3o8G43d-NtwTqzCmjoYXFxH2bvYmxA0i2L-EH6-_MzZaR7UBV_wJk130vYM0BIrgyWhWpNIHDf0xATBq6rMhVuhEUP4WLgiQQp_cAR0AJ1qZ2pcJyxCnWpDHSdg1D3vP734H6djM77guObVRmGrk5Xp2eRcC4EEEP1DsF53xHxR_ciH8mq1RO5G5GWmitVPzPrrZGYoD1XQudT300kk3mPGnEUP6uVs-JO4.ER5W3QEeONNz7lUZAAbLIg \ No newline at end of file diff --git a/client/keyring/testdata/keyring-file/keyhash b/client/keyring/testdata/keyring-file/keyhash new file mode 100644 index 00000000..5d41bad2 --- /dev/null +++ b/client/keyring/testdata/keyring-file/keyhash @@ -0,0 +1 @@ +$2a$10$dwTjlfhSfydxZcMd8dqWxOjZ06RgbCv3oCrmrkv8.M6jzKGCzx5r2 \ No newline at end of file diff --git a/client/keyring/testdata/keyring-file/test.info b/client/keyring/testdata/keyring-file/test.info new file mode 100644 index 00000000..5580cfdb --- /dev/null +++ b/client/keyring/testdata/keyring-file/test.info @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wOS0wNyAxMzo0OTowNi42MzkyNjIgKzAyMDAgQ0VTVCBtPSs4MS42MzY3ODE1NDMiLCJlbmMiOiJBMjU2R0NNIiwicDJjIjo4MTkyLCJwMnMiOiJGMUxoTER3ajhPR2VBRVh0In0.0Sa9gloQcB_4t5RAkS1kgqlCgBu0NwZG_WCHpA7eU3B7bD2zZjKM-A.uLO-mtT3vfO1pJLx.q7FZhW_tKnFTL80UuJU9LPj8T0nFS5UOcy_j23G3UGUs-gstVs9cJEtSNZOCz14-EcsRdJtWm5T9nx0Aauh_48LySQ-LDBbbycH1BJjyEMDhxn8zf0En8uIpZWiWa6vgfeomJE7BY_tRVFnMtzXvFJ69Ky37wFjSKeBac0XaxRc2XsBYUIRJY3xqnni53nvjw55fmyHo5-gBV-OC3ZDX4LmeTQcKw71LCVfGA1oxuk7BQcsYHc2_v2Lxr5rDuoZJl1Do32r63ss2fee85-q0Htjw_unaswZa0KLfMyyMOyRyTPsw0NHik8YjqkGJt9EOfARgu_IFGYoxaTotBBnjbpxOg7dpX4467WnFjkUbXtVFWqEAvIOhRmMbobUqJIPz1Ai5t3jYN-PIKfElgXciXdXPVEVe0j7ABJsNZCFJ11FueRqWhgTw_1B0OWrXjyWQ9TM-Yba9h_v7Cw0AP4lhvk6nm95H_pwYXX_dURj_5w.DUxlLK9M5aJbnHeYmWoHZQ \ No newline at end of file diff --git a/client/keyring/testdata/keyring-file/test2.info b/client/keyring/testdata/keyring-file/test2.info new file mode 100644 index 00000000..a78d4339 --- /dev/null +++ b/client/keyring/testdata/keyring-file/test2.info @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wOS0wNyAxMzo1MzozMi41MTY1MTcgKzAyMDAgQ0VTVCBtPSswLjUzMzkzMDc1MSIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjgxOTIsInAycyI6InFkWl9BbUtFUTg4UE8zb1EifQ.0XmW52t7Bn9bZKbjRLTLz0t8xPkgspFZNpzEgWgWaVUBugjlHqXm3w.mDeWYJuwq7t8UPy9.RDkxLQVylI0V1q1Ibihp73y5EnfqKYAwpQtdAKC9zcTOLVNIziYBjNBfgFKRbNE6q38AZo2_mN4GqH3o-9OwEWlj-qWe0H1EjXVuWqpteaT4EpYC7ZX9uMrk6yQyf_lfniGnL0f5j_hzMl_CVC6lGUgOf6nS1fOxZ-we00rcLNu-3W9ZZreTVwDQG-w2sCV95nreMTmkGd-z8BmgZsDDDB0YB0gnW_TDHVyn3zNi8S3SQxXEgKqXGi5KdsJTBQHkl6fx55LYb4o1sSvgiA8JcNtqtwvARNCq2tLS8ADGMMrrbIPEpmHS69Gd-6UEFpmP8vOeWKwKk52y9ozL___q9yMC-y71DkRpbOcM5zYEBZmStD2gucpYxlEJY5hEVN3szHQZz7MfW8MqiESlRi1-cRCanVDZOl3K6AbRix_F75yMxkWg3OEjwueecsKXFlqzz_Zv8MxMFJQWAYiEloXLZaNPS9GIUrWnALLO2BvySlE2lwf9.yzPjJMAGieybi_UccRHU2w \ No newline at end of file diff --git a/go.mod b/go.mod index 34e0b5b1..411a8b43 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/cometbft/cometbft v0.38.9 github.com/cosmos/cosmos-proto v1.0.0-beta.5 github.com/cosmos/cosmos-sdk v0.50.7 + github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogoproto v1.5.0 github.com/cosmos/ibc-go/modules/capability v1.0.0 github.com/cosmos/ibc-go/v8 v8.2.0 @@ -85,7 +86,6 @@ require ( github.com/cometbft/cometbft-db v0.9.1 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.0.2 // indirect - github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v1.1.2 // indirect github.com/cosmos/ics23/go v0.10.0 // indirect