Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions cmd/internal/wallet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package internal

import (
"fmt"
"os"

"github.com/hashicorp/go-secure-stdlib/password"

"github.com/spacemeshos/smcli/wallet"
)

// LoadWallet from a file, asks for the password from stdin.
func LoadWallet(path string, debug bool) (*wallet.Wallet, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}

defer f.Close()

fmt.Print("Enter wallet password: ")
pass, err := password.Read(os.Stdin)
fmt.Println()
if err != nil {
return nil, err
}

wk := wallet.NewKey(wallet.WithPasswordOnly([]byte(pass)))

w, err := wk.Open(f, debug)
if err != nil {
return nil, err
}

return w, nil
}
214 changes: 119 additions & 95 deletions cmd/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import (
"strings"

"github.com/btcsuite/btcutil/base58"
"github.com/cosmos/btcutil/bech32"
"github.com/hashicorp/go-secure-stdlib/password"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spf13/cobra"

"github.com/spacemeshos/smcli/cmd/internal"
"github.com/spacemeshos/smcli/common"
"github.com/spacemeshos/smcli/wallet"
)
Expand All @@ -31,11 +34,18 @@ var (
// printBase58 indicates that keys should be printed in base58 format.
printBase58 bool

// printHex indicates that keys should be printed in Hex format.
printHex bool

// printParent indicates that the parent key should be printed.
printParent bool

// useLedger indicates that the Ledger device should be used.
useLedger bool

// noAddress indicates that the address should not be shown when printing a key.
// This matches the old behavior of the read cmd.
noAddress bool
)

// walletCmd represents the wallet command.
Expand Down Expand Up @@ -139,34 +149,22 @@ sure the device is connected, unlocked, and the Spacemesh app is open.`,

// readCmd reads an existing wallet file.
var readCmd = &cobra.Command{
Use: "read [wallet file] [--full/-f] [--private/-p] [--base58]",
Short: "Reads an existing wallet file",
Use: "read [wallet file] [--full/-f] [--private/-p] [--parent] [--base58] [--hex] [--no-address]",
DisableFlagsInUseLine: true,
Short: "Reads an existing wallet file",
Long: `This command can be used to verify whether an existing wallet file can be
successfully read and decrypted, whether the password to open the file is correct, etc.
It prints the accounts from the wallet file. By default it does not print private keys.
Add --private to print private keys. Add --full to print full keys. Add --base58 to print
keys in base58 format rather than hexadecimal. Add --parent to print parent key (and not
only child keys).`,
keys in base58 format or --hex for hexdecimal rather than bech32. Add --parent to print parent key (and not
only child keys). Add --no-address to not print the address.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
walletFn := args[0]

// make sure the file exists
f, err := os.Open(walletFn)
cobra.CheckErr(err)
defer f.Close()

// get the password
fmt.Print("Enter wallet password: ")
password, err := password.Read(os.Stdin)
fmt.Println()
cobra.CheckErr(err)

// attempt to read it
wk := wallet.NewKey(wallet.WithPasswordOnly([]byte(password)))
w, err := wk.Open(f, debug)
w, err := internal.LoadWallet(args[0], debug)
cobra.CheckErr(err)

caption := make([]string, 0, 2)
maxWidth := 20
widthEnforcer := func(col string, maxLen int) string {
if len(col) <= maxLen {
return col
Expand All @@ -177,106 +175,128 @@ only child keys).`,
return fmt.Sprintf("%s..%s", col[:maxLen-7], col[len(col)-5:])
}

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetTitle("Wallet Contents")
caption := ""
if printPrivate {
caption = fmt.Sprintf("Mnemonic: %s", w.Mnemonic())
}
if !printFull {
if printPrivate {
caption += "\n"
}
caption += "To print full keys, use the --full flag."
}
t.SetCaption(caption)
maxWidth := 20
header := table.Row{"pubkey", "path", "name", "created"}

if printFull {
// full key is 64 bytes which is 128 chars in hex, need to print at least this much
maxWidth = 150
} else {
caption = append(caption, "To print full keys, use the --full flag.")
}

colCfgs := []table.ColumnConfig{
{Number: 1, WidthMax: maxWidth, WidthMaxEnforcer: widthEnforcer},
}
// TODO: add spacemesh address format (bech32)
// https://github.com/spacemeshos/smcli/issues/38

if !noAddress {
header = append(header[:3], header[2:]...)
header[2] = "address"
}

if printPrivate {
t.AppendHeader(table.Row{
"pubkey",
"privkey",
"path",
"name",
"created",
})
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, WidthMax: maxWidth, WidthMaxEnforcer: widthEnforcer},
{Number: 2, WidthMax: maxWidth, WidthMaxEnforcer: widthEnforcer},
})
} else {
t.AppendHeader(table.Row{
"pubkey",
"path",
"name",
"created",
})
t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, WidthMax: maxWidth, WidthMaxEnforcer: widthEnforcer},
caption = append(caption, fmt.Sprintf("Mnemonic: %s", w.Mnemonic()))
header = append(header[:2], header[1:]...)
header[1] = "privkey"
colCfgs = append(colCfgs, table.ColumnConfig{
Number: 2, WidthMax: maxWidth, WidthMaxEnforcer: widthEnforcer,
})
}

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetTitle("Wallet Contents")
t.SetCaption(strings.Join(caption, "\n"))
t.AppendHeader(header)
t.SetColumnConfigs(colCfgs)

// set the encoder
encoder := hex.EncodeToString
if printBase58 {
var encoder func([]byte) string
switch {
case printBase58:
encoder = base58.Encode
case printHex:
encoder = hex.EncodeToString
default:
encoder = func(data []byte) string {
dataConverted, err := bech32.ConvertBits(data, 8, 5, true)
cobra.CheckErr(err)
encoded, err := bech32.Encode(types.NetworkHRP(), dataConverted)
cobra.CheckErr(err)
return encoded
}
}

privKeyEncoder := func(privKey []byte) string {
if len(privKey) == 0 {
return "(none)"
addRow := func(account *wallet.EDKeyPair) {
row := make([]any, 0, 6) // Row len is 4 w/o address, up to 6 w/ priv key.
row = append(row, encoder(account.Public))

if printPrivate {
privKey := "(none)"
if len(account.Private) > 0 {
privKey = encoder(account.Private)
}

row = append(row, privKey)
}

row = append(row, account.Path.String())

if !noAddress {
row = append(row, types.GenerateAddress(account.Public).String())
}
return encoder(privKey)

row = append(row, account.DisplayName, account.Created)

t.AppendRow(row)
}

// print the master account
if printParent {
master := w.Secrets.MasterKeypair
if master != nil {
if printPrivate {
t.AppendRow(table.Row{
encoder(master.Public),
privKeyEncoder(master.Private),
master.Path.String(),
master.DisplayName,
master.Created,
})
} else {
t.AppendRow(table.Row{
encoder(master.Public),
master.Path.String(),
master.DisplayName,
master.Created,
})
}
if master := w.Secrets.MasterKeypair; master != nil {
addRow(master)
}
}

// print child accounts
for _, a := range w.Secrets.Accounts {
if printPrivate {
t.AppendRow(table.Row{
encoder(a.Public),
privKeyEncoder(a.Private),
a.Path.String(),
a.DisplayName,
a.Created,
})
} else {
addRow(a)
}

t.Render()
},
}

var addrCmd = &cobra.Command{
Use: "address [wallet file] [--parent]",
DisableFlagsInUseLine: true,
Short: "Show wallet addresses",
Long: "Show the addresses associated with the given wallet",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
w, err := internal.LoadWallet(args[0], debug)
cobra.CheckErr(err)

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetTitle("Wallet Addresses")
t.AppendHeader(table.Row{"address", "name"})

if printParent {
if master := w.Secrets.MasterKeypair; master != nil {
t.AppendRow(table.Row{
encoder(a.Public),
a.Path.String(),
a.DisplayName,
a.Created,
types.GenerateAddress(master.Public).String(),
master.DisplayName,
})
}
}

for _, account := range w.Secrets.Accounts {
t.AppendRow(table.Row{
types.GenerateAddress(account.Public).String(),
account.DisplayName,
})
}

t.Render()
},
}
Expand All @@ -285,10 +305,14 @@ func init() {
rootCmd.AddCommand(walletCmd)
walletCmd.AddCommand(createCmd)
walletCmd.AddCommand(readCmd)
walletCmd.AddCommand(addrCmd)
readCmd.Flags().BoolVarP(&printPrivate, "private", "p", false, "Print private keys")
readCmd.Flags().BoolVarP(&printFull, "full", "f", false, "Print full keys (no abbreviation)")
readCmd.Flags().BoolVar(&printBase58, "base58", false, "Print keys in base58 (rather than hex)")
readCmd.Flags().BoolVar(&printBase58, "base58", false, "Print keys in base58 (rather than bech32)")
readCmd.Flags().BoolVar(&printHex, "hex", false, "Print keys in hex (rather than bech32)")
readCmd.Flags().BoolVar(&printParent, "parent", false, "Print parent key (not only child keys)")
readCmd.Flags().BoolVar(&noAddress, "no-address", false, "Do not print the address associated with the key")
readCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "enable debug mode")
createCmd.Flags().BoolVarP(&useLedger, "ledger", "l", false, "Create a wallet using a Ledger device")
addrCmd.Flags().BoolVar(&printParent, "parent", false, "Print parent address (not only child addresses)")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.18

require (
github.com/btcsuite/btcutil v1.0.2
github.com/cosmos/btcutil v1.0.5
github.com/jedib0t/go-pretty/v6 v6.4.6
github.com/spacemeshos/economics v0.1.0
github.com/spacemeshos/go-spacemesh v1.0.2
Expand All @@ -15,7 +16,6 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/go-llsqlite/llsqlite v0.0.0-20230612031458-a9e271fe723a // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
Expand Down