mirror of https://github.com/guggero/chantools
cmd/chantools: add new createwallet subcommand
This commit adds a new subcommand for creating a new lnd compatible wallet.db file from an existing aezeed, master root key (xprv) or by generating a new aezeed.pull/124/head
parent
a3a00d410a
commit
b169634d85
@ -0,0 +1,230 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
||||
"github.com/lightninglabs/chantools/lnd"
|
||||
"github.com/lightningnetwork/lnd/aezeed"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createWalletCommand struct {
|
||||
WalletDBDir string
|
||||
GenerateSeed bool
|
||||
|
||||
rootKey *rootKey
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newCreateWalletCommand() *cobra.Command {
|
||||
cc := &createWalletCommand{}
|
||||
cc.cmd = &cobra.Command{
|
||||
Use: "createwallet",
|
||||
Short: "Create a new lnd compatible wallet.db file from an " +
|
||||
"existing seed or by generating a new one",
|
||||
Long: `Creates a new wallet that can be used with lnd or with
|
||||
chantools. The wallet can be created from an existing seed or a new one can be
|
||||
generated (use --generateseed).`,
|
||||
Example: `chantools createwallet \
|
||||
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet`,
|
||||
RunE: cc.Execute,
|
||||
}
|
||||
cc.cmd.Flags().StringVar(
|
||||
&cc.WalletDBDir, "walletdbdir", "", "the folder to create the "+
|
||||
"new wallet.db file in",
|
||||
)
|
||||
cc.cmd.Flags().BoolVar(
|
||||
&cc.GenerateSeed, "generateseed", false, "generate a new "+
|
||||
"seed instead of using an existing one",
|
||||
)
|
||||
|
||||
cc.rootKey = newRootKey(cc.cmd, "creating the new wallet")
|
||||
|
||||
return cc.cmd
|
||||
}
|
||||
|
||||
func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error {
|
||||
var (
|
||||
publicWalletPw = lnwallet.DefaultPublicPassphrase
|
||||
privateWalletPw = lnwallet.DefaultPrivatePassphrase
|
||||
masterRootKey *hdkeychain.ExtendedKey
|
||||
birthday time.Time
|
||||
err error
|
||||
)
|
||||
|
||||
// Check that we have a wallet DB.
|
||||
if c.WalletDBDir == "" {
|
||||
return fmt.Errorf("wallet DB directory is required")
|
||||
}
|
||||
|
||||
// Make sure the directory (and parents) exists.
|
||||
if err := os.MkdirAll(c.WalletDBDir, 0700); err != nil {
|
||||
return fmt.Errorf("error creating wallet DB directory '%s': %w",
|
||||
c.WalletDBDir, err)
|
||||
}
|
||||
|
||||
// Check if we should create a new seed or read if from the console or
|
||||
// environment.
|
||||
if c.GenerateSeed {
|
||||
fmt.Printf("Generating new lnd compatible aezeed...\n")
|
||||
seed, err := aezeed.New(
|
||||
keychain.KeyDerivationVersionTaproot, nil, time.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating new seed: %w", err)
|
||||
}
|
||||
birthday = seed.BirthdayTime()
|
||||
|
||||
// Derive the master extended key from the seed.
|
||||
masterRootKey, err = hdkeychain.NewMaster(
|
||||
seed.Entropy[:], chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive master extended "+
|
||||
"key: %w", err)
|
||||
}
|
||||
|
||||
passphrase, err := lnd.ReadPassphrase("shouldn't use")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading passphrase: %w", err)
|
||||
}
|
||||
|
||||
mnemonic, err := seed.ToMnemonic(passphrase)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting seed to "+
|
||||
"mnemonic: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Generated new seed")
|
||||
printCipherSeedWords(mnemonic[:])
|
||||
} else {
|
||||
masterRootKey, birthday, err = c.rootKey.readWithBirthday()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// To automate things with chantools, we also offer reading the wallet
|
||||
// password from environment variables.
|
||||
pw := []byte(strings.TrimSpace(os.Getenv(passwordEnvName)))
|
||||
|
||||
// Because we cannot differentiate between an empty and a non-existent
|
||||
// environment variable, we need a special character that indicates that
|
||||
// no password should be used. We use a single dash (-) for that as that
|
||||
// would be too short for an explicit password anyway.
|
||||
switch {
|
||||
// The user indicated in the environment variable that no passphrase
|
||||
// should be used. We don't set any value.
|
||||
case string(pw) == "-":
|
||||
|
||||
// The environment variable didn't contain anything, we'll read the
|
||||
// passphrase from the terminal.
|
||||
case len(pw) == 0:
|
||||
fmt.Printf("\n\nThe wallet password is used to encrypt the " +
|
||||
"wallet.db file itself and is unrelated to the seed.\n")
|
||||
pw, err = passwordFromConsole("Input new wallet password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw2, err := passwordFromConsole("Confirm new wallet password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(pw, pw2) {
|
||||
return fmt.Errorf("passwords don't match")
|
||||
}
|
||||
|
||||
if len(pw) > 0 {
|
||||
publicWalletPw = pw
|
||||
privateWalletPw = pw
|
||||
}
|
||||
|
||||
// There was a password in the environment, just use it directly.
|
||||
default:
|
||||
publicWalletPw = pw
|
||||
privateWalletPw = pw
|
||||
}
|
||||
|
||||
// Try to create the wallet.
|
||||
loader, err := btcwallet.NewWalletLoader(
|
||||
chainParams, 0, btcwallet.LoaderWithLocalWalletDB(
|
||||
c.WalletDBDir, true, 0,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating wallet loader: %w", err)
|
||||
}
|
||||
|
||||
_, err = loader.CreateNewWalletExtendedKey(
|
||||
publicWalletPw, privateWalletPw, masterRootKey, birthday,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating new wallet: %w", err)
|
||||
}
|
||||
|
||||
if err := loader.UnloadWallet(); err != nil {
|
||||
return fmt.Errorf("error unloading wallet: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Wallet created successfully at %v\n", c.WalletDBDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printCipherSeedWords(mnemonicWords []string) {
|
||||
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
||||
"RESTORE THE WALLET!!!")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
|
||||
|
||||
numCols := 4
|
||||
colWords := monoWidthColumns(mnemonicWords, numCols)
|
||||
for i := 0; i < len(colWords); i += numCols {
|
||||
fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n",
|
||||
i+1, colWords[i], i+2, colWords[i+1], i+3,
|
||||
colWords[i+2], i+4, colWords[i+3])
|
||||
}
|
||||
|
||||
fmt.Println("---------------END LND CIPHER SEED-----------------")
|
||||
|
||||
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
||||
"RESTORE THE WALLET!!!")
|
||||
}
|
||||
|
||||
// monoWidthColumns takes a set of words, and the number of desired columns,
|
||||
// and returns a new set of words that have had white space appended to the
|
||||
// word in order to create a mono-width column.
|
||||
func monoWidthColumns(words []string, ncols int) []string {
|
||||
// Determine max size of words in each column.
|
||||
colWidths := make([]int, ncols)
|
||||
for i, word := range words {
|
||||
col := i % ncols
|
||||
curWidth := colWidths[col]
|
||||
if len(word) > curWidth {
|
||||
colWidths[col] = len(word)
|
||||
}
|
||||
}
|
||||
|
||||
// Append whitespace to each word to make columns mono-width.
|
||||
finalWords := make([]string, len(words))
|
||||
for i, word := range words {
|
||||
col := i % ncols
|
||||
width := colWidths[col]
|
||||
|
||||
diff := width - len(word)
|
||||
finalWords[i] = word + strings.Repeat(" ", diff)
|
||||
}
|
||||
|
||||
return finalWords
|
||||
}
|
Loading…
Reference in New Issue