Skip to content

Commit

Permalink
feat(cli): extend keys show command to display did:key info
Browse files Browse the repository at this point in the history
  • Loading branch information
ccamel committed Feb 10, 2024
1 parent c44b87a commit 631a636
Show file tree
Hide file tree
Showing 4 changed files with 310 additions and 92 deletions.
88 changes: 2 additions & 86 deletions client/keys/list.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
package keys

import (
"encoding/json"
"fmt"
"io"

"github.com/spf13/cobra"
"sigs.k8s.io/yaml"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/keys"
cryptokeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"

"github.com/okp4/okp4d/x/logic/util"
)

const (
flagListNames = "list-names"
ListCmdName = "list"
)

// KeyOutput is the output format for keys when listing them.
Expand All @@ -28,21 +19,8 @@ type KeyOutput struct {
DID string `json:"did,omitempty" yaml:"did"`
}

// ListKeysCmd lists all keys in the key store with additional info, such as the did:key equivalent of the public key.
// This is an improved copy of the ListKeysCmd from the keys module.
func ListKeysCmd() *cobra.Command {
cmd := &cobra.Command{
Use: ListCmdName,
Short: "List all keys",
Long: `Return a list of all public keys stored by this key manager
along with their associated name, address and decentralized identifier (for supported public key algorithms)`,
RunE: runListCmd,
}

cmd.Flags().BoolP(flagListNames, "n", false, "List names only")
return cmd
}

// runListCmd retrieves all keys from the keyring and prints them to the console.
// This is an improved copy of the runListCmd from the keys module of the cosmos-sdk.
func runListCmd(cmd *cobra.Command, _ []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
Expand All @@ -69,65 +47,3 @@ func runListCmd(cmd *cobra.Command, _ []string) error {

return nil
}

func printKeyringRecords(w io.Writer, records []*cryptokeyring.Record, output string) error {
kos, err := mkKeyOutput(records)
if err != nil {
return err
}

switch output {
case flags.OutputFormatText:
if err := printTextRecords(w, kos); err != nil {
return err
}

case flags.OutputFormatJSON:
out, err := json.Marshal(kos)
if err != nil {
return err
}

if _, err := fmt.Fprintf(w, "%s", out); err != nil {
return err
}
}

return nil
}

func printTextRecords(w io.Writer, kos []KeyOutput) error {
out, err := yaml.Marshal(&kos)
if err != nil {
return err
}

if _, err := fmt.Fprintln(w, string(out)); err != nil {
return err
}

return nil
}

func mkKeyOutput(records []*cryptokeyring.Record) ([]KeyOutput, error) {
kos := make([]KeyOutput, len(records))

for i, r := range records {
kko, err := keys.MkAccKeyOutput(r)
if err != nil {
return nil, err
}
pk, err := r.GetPubKey()
if err != nil {
return nil, err
}
did, _ := util.CreateDIDKeyByPubKey(pk)

kos[i] = KeyOutput{
KeyOutput: kko,
DID: did,
}
}

return kos, nil
}
20 changes: 14 additions & 6 deletions client/keys/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@ import (
"github.com/spf13/cobra"
)

const (
listKeysCmd = "list"
showKeysCmd = "show"
)

// Enhance augment the given command which is assumed to be the root command of the 'list' command.
// It will:
// - add the 'did' command.
// - replace the original 'list' command with our own 'list' command which will list all did:key.
// - replace the original 'list' command implementation with our own 'list' command which will list all did:key.
// - replace the original 'show' command implementation with our own 'show' command which will show the did:key of the key.
func Enhance(cmd *cobra.Command) *cobra.Command {
cmd.AddCommand(
DIDCmd(),
)

for i, c := range cmd.Commands() {
if c.Name() == ListCmdName {
cmd.RemoveCommand(cmd.Commands()[i])
cmd.AddCommand(ListKeysCmd())
break
for _, c := range cmd.Commands() {
switch c.Name() {
case listKeysCmd:
c.RunE = runListCmd
case showKeysCmd:
c.RunE = runShowCmd
default:
}
}

Expand Down
180 changes: 180 additions & 0 deletions client/keys/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package keys

import (
"errors"
"fmt"

"github.com/spf13/cobra"

errorsmod "cosmossdk.io/errors"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/keys"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
"github.com/cosmos/cosmos-sdk/crypto/ledger"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerr "github.com/cosmos/cosmos-sdk/types/errors"
)

const (
flagMultiSigThreshold = "multisig-threshold"
)

func runShowCmd(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

var keyRecord *keyring.Record
if len(args) == 1 {
keyRecord, err = fetchKey(clientCtx.Keyring, args[0])
if err != nil {
return fmt.Errorf("%s is not a valid name or address: %w", args[0], err)
}
} else {
keyRecord, err = fetchMultiSigKey(cmd, clientCtx, args)
if err != nil {
return err
}
}

if err := checkFlagCompatibility(cmd); err != nil {
return err
}

bechPrefix, _ := cmd.Flags().GetString(keys.FlagBechPrefix)
bechKeyOut, err := getBechKeyOut(bechPrefix)
if err != nil {
return err
}

if err := processOutput(cmd, clientCtx, keyRecord, bechKeyOut); err != nil {
return err
}

return nil
}

func fetchMultiSigKey(cmd *cobra.Command, clientCtx client.Context, args []string) (*keyring.Record, error) {
pks := make([]cryptotypes.PubKey, len(args))
for i, keyRef := range args {
k, err := fetchKey(clientCtx.Keyring, keyRef)
if err != nil {
return nil, fmt.Errorf("%s is not a valid name or address: %w", keyRef, err)
}
pubKey, err := k.GetPubKey()
if err != nil {
return nil, err
}
pks[i] = pubKey
}

multisigThreshold, _ := cmd.Flags().GetInt(flagMultiSigThreshold)
if err := validateMultisigThreshold(multisigThreshold, len(args)); err != nil {
return nil, err
}

multiKey := multisig.NewLegacyAminoPubKey(multisigThreshold, pks)
return keyring.NewMultiRecord(args[0], multiKey)
}

func checkFlagCompatibility(cmd *cobra.Command) error {
isShowAddr, _ := cmd.Flags().GetBool(keys.FlagAddress)
isShowPubKey, _ := cmd.Flags().GetBool(keys.FlagPublicKey)
if isShowAddr && isShowPubKey {
return errors.New("cannot use both --address and --pubkey at once")
}

isOutputSet := cmd.Flag(flags.FlagOutput) != nil && cmd.Flag(flags.FlagOutput).Changed
if isOutputSet && (isShowAddr || isShowPubKey) {
return errors.New("cannot use --output with --address or --pubkey")
}

return nil
}

func processOutput(cmd *cobra.Command, clientCtx client.Context, k *keyring.Record, bechKeyOut bechKeyOutFn) error {
isShowAddr, _ := cmd.Flags().GetBool(keys.FlagAddress)
isShowPubKey, _ := cmd.Flags().GetBool(keys.FlagPublicKey)
isShowDevice, _ := cmd.Flags().GetBool(keys.FlagDevice)

if isShowDevice {
return handleDeviceOutput(k)
}

if isShowAddr || isShowPubKey {
ko, err := bechKeyOut(k)
if err != nil {
return err
}
out := ko.Address
if isShowPubKey {
out = ko.PubKey
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
return err
}

outputFormat := clientCtx.OutputFormat
return printKeyringRecord(cmd.OutOrStdout(), k, bechKeyOut, outputFormat)
}

func handleDeviceOutput(k *keyring.Record) error {
if k.GetType() != keyring.TypeLedger {
return fmt.Errorf("the device flag (-d) can only be used for ledger keys")
}
ledgerItem := k.GetLedger()
if ledgerItem == nil {
return errors.New("unable to get ledger item")
}
pk, err := k.GetPubKey()
if err != nil {
return err
}
bechPrefix := sdk.GetConfig().GetBech32AccountAddrPrefix()
return ledger.ShowAddress(*ledgerItem.Path, pk, bechPrefix)
}

func fetchKey(kb keyring.Keyring, keyref string) (*keyring.Record, error) {
k, err := kb.Key(keyref)

if err == nil || !errorsmod.IsOf(err, sdkerr.ErrIO, sdkerr.ErrKeyNotFound) {
return k, err
}

accAddr, err := sdk.AccAddressFromBech32(keyref)
if err != nil {
return k, err
}

k, err = kb.KeyByAddress(accAddr)
return k, errorsmod.Wrap(err, "Invalid key")
}

func validateMultisigThreshold(k, nKeys int) error {
if k <= 0 {
return fmt.Errorf("threshold must be a positive integer")
}
if nKeys < k {
return fmt.Errorf(
"threshold k of n multisignature: %d < %d", nKeys, k)
}
return nil
}

func getBechKeyOut(bechPrefix string) (bechKeyOutFn, error) {
switch bechPrefix {
case sdk.PrefixAccount:
return keys.MkAccKeyOutput, nil
case sdk.PrefixValidator:
return keys.MkValKeyOutput, nil
case sdk.PrefixConsensus:
return keys.MkConsKeyOutput, nil
}

return nil, fmt.Errorf("invalid Bech32 prefix encoding provided: %s", bechPrefix)
}
Loading

0 comments on commit 631a636

Please sign in to comment.