diff --git a/client/keys/add.go b/client/keys/add.go index 11a6326a..334ffd67 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -306,6 +306,11 @@ func printCreate(cmd *cobra.Command, info keyring.Info, showMnemonic bool, mnemo return err } + out, err = keyring.PopulateEvmAddrIfApplicable(info, out) + if err != nil { + return err + } + if showMnemonic { out.Mnemonic = mnemonic } diff --git a/client/keys/add_test.go b/client/keys/add_test.go index f59848a8..13f81656 100644 --- a/client/keys/add_test.go +++ b/client/keys/add_test.go @@ -273,3 +273,66 @@ func TestAddRecoverFileBackend(t *testing.T) { require.NoError(t, err) require.Equal(t, "keyname1", info.GetName()) } + +func Test_runAddCmdJSONEvmAddress(t *testing.T) { + cmd := AddKeyCommand() + cmd.Flags().AddFlagSet(Commands("home").PersistentFlags()) + + kbHome := t.TempDir() + mockIn := testutil.ApplyMockIODiscardOutErr(cmd) + + clientCtx := client.Context{}.WithKeyringDir(kbHome).WithInput(mockIn) + ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx) + + b := bytes.NewBufferString("") + cmd.SetOut(b) + + cmd.SetArgs([]string{ + "test-evm-key", + fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome), + fmt.Sprintf("--%s=%s", cli.OutputFlag, OutputFormatJSON), + fmt.Sprintf("--%s=%s", flags.FlagKeyAlgorithm, string(hd.Secp256k1Type)), + fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest), + }) + + require.NoError(t, cmd.ExecuteContext(ctx)) + + // Check that the JSON output contains an EVM address + output, err := ioutil.ReadAll(b) + require.NoError(t, err) + + outputStr := string(output) + require.Contains(t, outputStr, `"evm_address"`) + require.Contains(t, outputStr, `"0x`) + require.NotContains(t, outputStr, `"evm_address":""`) + require.NotContains(t, outputStr, `"evm_address":null`) +} + +func Test_PopulateEvmAddrError_JSONOutput(t *testing.T) { + // This test verifies that if PopulateEvmAddrIfApplicable returns an error, + // the add command properly handles it and returns the error + + cmd := AddKeyCommand() + cmd.Flags().AddFlagSet(Commands("home").PersistentFlags()) + + kbHome := t.TempDir() + mockIn := testutil.ApplyMockIODiscardOutErr(cmd) + + clientCtx := client.Context{}.WithKeyringDir(kbHome).WithInput(mockIn) + ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx) + + // Create a key with sr25519 algorithm - this should fail when PopulateEvmAddrIfApplicable + // tries to parse the sr25519 private key as secp256k1 + cmd.SetArgs([]string{ + "test-sr25519-key", + fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome), + fmt.Sprintf("--%s=%s", cli.OutputFlag, OutputFormatJSON), + fmt.Sprintf("--%s=%s", flags.FlagKeyAlgorithm, string(hd.Sr25519Type)), + fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest), + }) + + // This should fail because sr25519 keys can't be used to generate EVM addresses + err := cmd.ExecuteContext(ctx) + require.Error(t, err) + require.Contains(t, err.Error(), "unmarshal to types.PrivKey failed") +} diff --git a/crypto/keyring/output.go b/crypto/keyring/output.go index f8dd2a7f..a1d6f4e0 100644 --- a/crypto/keyring/output.go +++ b/crypto/keyring/output.go @@ -92,20 +92,28 @@ func MkAccKeysOutput(infos []Info) ([]KeyOutput, error) { } func PopulateEvmAddrIfApplicable(info Info, o KeyOutput) (KeyOutput, error) { - localInfo, ok := info.(LocalInfo) - if ok { - // Only works with secp256k1 algo - priv, err := legacy.PrivKeyFromBytes([]byte(localInfo.PrivKeyArmor)) - if err != nil { - return o, err - } - privHex := hex.EncodeToString(priv.Bytes()) - privKey, err := crypto.HexToECDSA(privHex) - if err != nil { - return o, err - } - o.EvmAddress = crypto.PubkeyToAddress(privKey.PublicKey).Hex() - } else { + var localInfo LocalInfo + switch v := info.(type) { + case LocalInfo: + localInfo = v + case *LocalInfo: + localInfo = *v + default: + return o, nil // non-local key – nothing to do } + + // Only secp256k1 keys produce an EVM address + priv, err := legacy.PrivKeyFromBytes([]byte(localInfo.PrivKeyArmor)) + if err != nil { + return o, err + } + + privHex := hex.EncodeToString(priv.Bytes()) + privKey, err := crypto.HexToECDSA(privHex) + if err != nil { + return o, err + } + + o.EvmAddress = crypto.PubkeyToAddress(privKey.PublicKey).Hex() return o, nil } diff --git a/crypto/keyring/output_test.go b/crypto/keyring/output_test.go index ab179c65..20d87ecb 100644 --- a/crypto/keyring/output_test.go +++ b/crypto/keyring/output_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/cosmos/cosmos-sdk/codec/legacy" + "github.com/cosmos/cosmos-sdk/crypto/hd" kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" "github.com/cosmos/cosmos-sdk/crypto/types" @@ -45,3 +47,114 @@ func TestMkAccKeyOutputForSr25519(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedOutput, out) } + +func TestPopulateEvmAddrIfApplicable(t *testing.T) { + sk := secp256k1.GenPrivKey() + pubKey := sk.PubKey() + + // PrivKeyArmor should contain amino-encoded private key bytes + aminoBytes, err := legacy.Cdc.Marshal(sk) + require.NoError(t, err) + privKeyArmor := string(aminoBytes) + + tests := []struct { + name string + info Info + input KeyOutput + expectError bool + expectEvm bool + }{ + { + name: "LocalInfo pointer case - should populate EVM address", + info: &LocalInfo{ + Name: "test-key", + PubKey: pubKey, + PrivKeyArmor: privKeyArmor, + Algo: hd.Secp256k1Type, + }, + input: KeyOutput{ + Name: "test-key", + Type: "local", + Address: sdk.AccAddress(pubKey.Address()).String(), + PubKey: "", + }, + expectError: false, + expectEvm: true, + }, + { + name: "LocalInfo value case - should populate EVM address", + info: LocalInfo{ + Name: "test-key", + PubKey: pubKey, + PrivKeyArmor: privKeyArmor, + Algo: hd.Secp256k1Type, + }, + input: KeyOutput{ + Name: "test-key", + Type: "local", + Address: sdk.AccAddress(pubKey.Address()).String(), + PubKey: "", + }, + expectError: false, + expectEvm: true, + }, + { + name: "Non-LocalInfo case - should return unchanged", + info: &multiInfo{ + Name: "multi-key", + PubKey: pubKey, + Threshold: 1, + }, + input: KeyOutput{ + Name: "multi-key", + Type: "multi", + Address: sdk.AccAddress(pubKey.Address()).String(), + EvmAddress: "0x1234567890123456789012345678901234567890", + PubKey: "", + }, + expectError: false, + expectEvm: false, + }, + { + name: "Invalid private key armor - should return error", + info: &LocalInfo{ + Name: "bad-key", + PubKey: pubKey, + PrivKeyArmor: "invalid-armor", + Algo: hd.Secp256k1Type, + }, + input: KeyOutput{ + Name: "bad-key", + Type: "local", + Address: sdk.AccAddress(pubKey.Address()).String(), + PubKey: "", + }, + expectError: true, + expectEvm: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PopulateEvmAddrIfApplicable(tt.info, tt.input) + + if tt.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.input.Name, result.Name) + require.Equal(t, tt.input.Type, result.Type) + require.Equal(t, tt.input.Address, result.Address) + + if tt.expectEvm { + require.NotEmpty(t, result.EvmAddress) + require.Len(t, result.EvmAddress, 42) // 0x + 40 hex chars + require.True(t, result.EvmAddress[:2] == "0x") + } else { + require.Equal(t, tt.input.EvmAddress, result.EvmAddress) + } + }) + } +}