aergoio/aergo

View on GitHub
cmd/aergocli/cmd/accounts.go

Summary

Maintainability
A
2 hrs
Test Coverage
F
43%
package cmd

import (
    "context"
    "errors"
    "fmt"
    "io"
    "log"
    "os"
    "path/filepath"
    "syscall"

    "github.com/aergoio/aergo/v2/account/key"
    "github.com/aergoio/aergo/v2/types"
    "github.com/spf13/cobra"
    "golang.org/x/crypto/ssh/terminal"
)

func init() {
    accountCmd := &cobra.Command{
        Use:               "account [flags] subcommand",
        Short:             "Account command",
        PersistentPreRun:  preConnectAergo,
        PersistentPostRun: disconnectAergo,
    }

    newCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    unlockCmd.Flags().StringVar(&address, "address", "", "address of account")
    unlockCmd.MarkFlagRequired("address")
    unlockCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    lockCmd.Flags().StringVar(&address, "address", "", "address of account")
    lockCmd.MarkFlagRequired("address")
    lockCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    importCmd.Flags().StringVar(&importFormat, "if", "", "Base58 import format string")
    importCmd.Flags().StringVar(&pw, "password", "", "password used when exporting")
    importCmd.Flags().StringVar(&to, "newpassword", "", "new password for storing account")
    importCmd.Flags().StringVar(&importFilePath, "path", "", "path to import keystore file")

    exportCmd.Flags().StringVar(&address, "address", "", "Address of account")
    exportCmd.MarkFlagRequired("address")
    exportCmd.Flags().BoolVar(&exportAsWif, "wif", false, "export as encrypted string instead of keystore format")
    exportCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    voteCmd.Flags().StringVar(&address, "address", "", "account address of voter")
    voteCmd.MarkFlagRequired("address")
    voteCmd.Flags().StringVar(&to, "to", "", "json string array which has candidates, or input file path")
    voteCmd.MarkFlagRequired("to")
    voteCmd.Flags().StringVar(&voteId, "id", types.OpvoteBP.Cmd(), "id to vote (e.g. bpcount, stakingmin, gasprice, nameprice)")
    voteCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    stakeCmd.Flags().StringVar(&address, "address", "", "account address")
    stakeCmd.MarkFlagRequired("address")
    stakeCmd.Flags().StringVar(&amount, "amount", "0", "amount to stake")
    stakeCmd.MarkFlagRequired("amount")
    stakeCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    unstakeCmd.Flags().StringVar(&address, "address", "", "account address")
    unstakeCmd.MarkFlagRequired("address")
    unstakeCmd.Flags().StringVar(&amount, "amount", "0", "amount to unstake")
    unstakeCmd.MarkFlagRequired("amount")
    unstakeCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    accountCmd.AddCommand(newCmd, listCmd, unlockCmd, lockCmd, importCmd, exportCmd, voteCmd, stakeCmd, unstakeCmd)
    rootCmd.AddCommand(accountCmd)
}

var newCmd = &cobra.Command{
    Use:   "new [flags]",
    Short: "Create new account in the node or cli",
    Run: func(cmd *cobra.Command, args []string) {
        var param types.Personal
        var err error
        if pw != "" {
            param.Passphrase = pw
        } else {
            param.Passphrase, err = getPasswd(cmd, true)
            if err != nil {
                cmd.PrintErrf("Failed get password: %s\n", err.Error())
                return
            }
        }
        var msg *types.Account
        var addr []byte
        if rootConfig.KeyStorePath == "" {
            msg, err = client.CreateAccount(context.Background(), &param)
            if msg != nil {
                addr = msg.GetAddress()
            }
        } else {
            dataEnvPath := os.ExpandEnv(rootConfig.KeyStorePath)
            ks := key.NewStore(dataEnvPath, 0)
            defer ks.CloseStore()
            addr, err = ks.CreateKey(param.Passphrase)
            if err != nil {
                cmd.PrintErrf("Failed: %s\n", err.Error())
                return
            }
        }
        if err != nil {
            cmd.PrintErrf("Failed: %s\n", err.Error())
            return
        }
        cmd.Println(types.EncodeAddress(addr))
    },
}

var listCmd = &cobra.Command{
    Use:   "list [flags]",
    Short: "Get account list in the node or cli",
    Run: func(cmd *cobra.Command, args []string) {
        var err error
        var msg *types.AccountList
        var addrs [][]byte
        if rootConfig.KeyStorePath == "" {
            msg, err = client.GetAccounts(context.Background(), &types.Empty{})
        } else {
            dataEnvPath := os.ExpandEnv(rootConfig.KeyStorePath)
            ks := key.NewStore(dataEnvPath, 0)
            defer ks.CloseStore()
            addrs, err = ks.GetAddresses()
        }
        if err != nil {
            cmd.PrintErrf("Failed: %s\n", err.Error())
            return
        }
        out := fmt.Sprintf("%s", "[")
        if msg != nil {
            addresslist := msg.GetAccounts()
            for _, a := range addresslist {
                out = fmt.Sprintf("%s\"%s\", ", out, types.EncodeAddress(a.Address))
            }
            if addresslist != nil {
                out = out[:len(out)-2]
            }
        } else if addrs != nil && len(addrs) > 0 {
            for _, a := range addrs {
                out = fmt.Sprintf("%s\"%s\", ", out, types.EncodeAddress(a))
            }
            out = out[:len(out)-2]
        }
        out = fmt.Sprintf("%s%s", out, "]")
        cmd.Println(out)
    },
}

var lockCmd = &cobra.Command{
    Use:   "lock [flags]",
    Short: "Lock account in the node",
    Run: func(cmd *cobra.Command, args []string) {
        if rootConfig.KeyStorePath != "" {
            cmd.PrintErrf("There is no lock on the local keystore.\n")
            return
        }
        param, err := parsePersonalParam(cmd)
        if err != nil {
            cmd.PrintErrf("Failed: %s\n", err.Error())
            return
        }
        msg, err := client.LockAccount(context.Background(), param)
        if err != nil {
            cmd.PrintErrf("Failed: %s\n", err.Error())
            return
        }
        cmd.Println(types.EncodeAddress(msg.GetAddress()))
    },
}

var unlockCmd = &cobra.Command{
    Use:   "unlock [flags]",
    Short: "Unlock account in the node",
    Run: func(cmd *cobra.Command, args []string) {
        if rootConfig.KeyStorePath != "" {
            cmd.PrintErrf("Local keystore do not need to be unlocked.\n")
            return
        }
        param, err := parsePersonalParam(cmd)
        msg, err := client.UnlockAccount(context.Background(), param)
        if err != nil {
            cmd.PrintErrf("Failed: %s\n", err.Error())
            return
        }
        cmd.Println(types.EncodeAddress(msg.GetAddress()))
    },
}

// import account using WIF (legacy)
func importWif(cmd *cobra.Command) ([]byte, error) {
    var err error
    var address []byte
    importBuf, err := types.DecodePrivKey(importFormat)
    if err != nil {
        return nil, fmt.Errorf("failed to decode input: %s", err.Error())
    }
    wif := &types.ImportFormat{Wif: &types.SingleBytes{Value: importBuf}}
    if pw != "" {
        wif.Oldpass = pw
    } else {
        wif.Oldpass, err = getPasswd(cmd, false)
        if err != nil {
            return nil, err
        }
    }

    if to != "" {
        wif.Newpass = to
    } else {
        wif.Newpass = wif.Oldpass
    }

    if rootConfig.KeyStorePath == "" {
        msg, errRemote := client.ImportAccount(context.Background(), wif)
        address = msg.GetAddress()
        if errRemote != nil {
            return address, fmt.Errorf("node request returned error: %s", errRemote.Error())
        }
    } else {
        dataEnvPath := os.ExpandEnv(rootConfig.KeyStorePath)
        ks := key.NewStore(dataEnvPath, 0)
        defer ks.CloseStore()
        address, err = ks.ImportKey(importBuf, wif.Oldpass, wif.Newpass)
        if err != nil {
            return address, err
        }
    }
    return address, nil
}

// import account using keystore
func importKeystore(cmd *cobra.Command) ([]byte, error) {
    var err error
    var address []byte
    absPath, err := filepath.Abs(importFilePath)
    if err != nil {
        return nil, err
    }
    keystore, err := os.ReadFile(absPath)
    if err != nil {
        return nil, err
    }

    wif := &types.ImportFormat{Keystore: &types.SingleBytes{Value: keystore}}
    if pw != "" {
        wif.Oldpass = pw
    } else {
        wif.Oldpass, err = getPasswd(cmd, false)
        if err != nil {
            return nil, err
        }
    }

    if to != "" {
        wif.Newpass = to
    } else {
        wif.Newpass = wif.Oldpass
    }

    if rootConfig.KeyStorePath == "" {
        msg, errRemote := client.ImportAccount(context.Background(), wif)
        if errRemote != nil {
            return nil, errRemote
        }
        address = msg.GetAddress()
    } else {
        dataEnvPath := os.ExpandEnv(rootConfig.KeyStorePath)
        ks := key.NewStore(dataEnvPath, 0)
        defer ks.CloseStore()
        privateKey, err := key.LoadKeystore(keystore, wif.Oldpass)
        if err != nil {
            return nil, err
        }
        address, err = ks.AddKey(privateKey, wif.Newpass)
        if err != nil {
            return nil, err
        }
    }

    return address, nil
}

var importCmd = &cobra.Command{
    Use:   "import [flags]",
    Short: "Import account",
    Run: func(cmd *cobra.Command, args []string) {
        var address []byte
        var err error
        if importFormat != "" {
            address, err = importWif(cmd)
        } else if importFilePath != "" {
            address, err = importKeystore(cmd)
        } else {
            cmd.Help()
        }

        if err != nil {
            cmd.PrintErrln(err)
        }
        if address != nil {
            cmd.Println(types.EncodeAddress(address))
        }
    },
}

// export account as WIF (legacy)
func exportWif(cmd *cobra.Command, param *types.Personal) ([]byte, error) {
    if rootConfig.KeyStorePath == "" {
        msg, err := client.ExportAccount(context.Background(), param)
        if err != nil {
            return nil, fmt.Errorf("node request returned error: %s", err.Error())
        }
        return msg.Value, nil
    } else {
        dataEnvPath := os.ExpandEnv(rootConfig.KeyStorePath)
        ks := key.NewStore(dataEnvPath, 0)
        defer ks.CloseStore()
        wif, err := ks.ExportKey(param.Account.Address, param.Passphrase)
        if err != nil {
            return nil, err
        }
        return wif, nil
    }
}

// export account as keystore
func exportKeystore(cmd *cobra.Command, param *types.Personal) ([]byte, error) {
    if rootConfig.KeyStorePath == "" {
        msg, err := client.ExportAccountKeystore(context.Background(), param)
        if err != nil {
            return nil, fmt.Errorf("node request returned error: %s", err.Error())
        }
        return msg.Value, nil
    } else {
        dataEnvPath := os.ExpandEnv(rootConfig.KeyStorePath)
        ks := key.NewStore(dataEnvPath, 0)
        defer ks.CloseStore()

        privateKey, err := ks.GetKey(param.Account.Address, param.Passphrase)
        if err != nil {
            return nil, err
        }

        wif, err := key.GetKeystore(privateKey, param.Passphrase)
        if err != nil {
            return nil, err
        }
        return wif, nil
    }
}

var exportCmd = &cobra.Command{
    Use:   "export [flags]",
    Short: "Export account",
    Run: func(cmd *cobra.Command, args []string) {
        var result []byte
        var err error

        param, err := parsePersonalParam(cmd)
        if err != nil {
            cmd.PrintErr(err.Error())
            return
        }

        if exportAsWif {
            result, err = exportWif(cmd, param)
            if result != nil {
                cmd.Println(types.EncodePrivKey(result))
            }
        } else {
            result, err = exportKeystore(cmd, param)
            if result != nil {
                cmd.Println(string(result))
            }
        }

        if err != nil {
            cmd.PrintErrln(err)
        }
    },
}

func parsePersonalParam(cmd *cobra.Command) (*types.Personal, error) {
    var err error
    param := &types.Personal{Account: &types.Account{}}
    if address != "" {
        param.Account.Address, err = types.DecodeAddress(address)
        if err != nil {
            return nil, err
        }
        if pw != "" {
            param.Passphrase = pw
        } else {
            param.Passphrase, err = getPasswd(cmd, false)
            if err != nil {
                return nil, err
            }
        }
    }
    return param, nil
}

// Try to open /dev/tty for read/write, with stdin/stdout as fallback
// This is so we can use aergocli in a pipe and still read passwords from tty
func getTerminalReaderWriter() (struct {
    io.Reader
    io.Writer
}, int) {
    path := "/dev/tty"
    fallback := struct {
        io.Reader
        io.Writer
    }{os.Stdin, os.Stdout}
    in, err := os.Open(path)
    if err != nil {
        return fallback, 0
    }
    out, err := os.OpenFile(path, syscall.O_WRONLY, 0)
    if err != nil {
        return fallback, 0
    }
    return struct {
        io.Reader
        io.Writer
    }{in, out}, int(out.Fd())
}

func getPasswd(cmd *cobra.Command, isNew bool) (string, error) {
    screen, fd := getTerminalReaderWriter()
    oldState, err := terminal.MakeRaw(fd)
    if err != nil {
        return "", err
    }
    defer terminal.Restore(0, oldState)
    term := terminal.NewTerminal(screen, "")
    password, err := term.ReadPassword("Enter password: ")
    if err != nil {
        return "", err
    }
    if len(password) == 0 {
        return "", errors.New("empty password")
    }
    if isNew {
        repeat, err := term.ReadPassword("Repeat password: ")
        if err != nil {
            return "", err
        }
        if password != repeat {
            return "", errors.New("Passwords don't match")
        }
    }
    return password, err
}

func preConnectAergo(cmd *cobra.Command, args []string) {
    if rootConfig.KeyStorePath == "" {
        connectAergo(cmd, args)
    } else {
        client = nil
    }

    if len(sock) > 0 {
        if admClient = newAergoAdminClient(sock); admClient == nil {
            log.Fatal("can't create admin connection")
        }
    }
}