|  | 
|  | 1 | +package main | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"bytes" | 
|  | 5 | +	"fmt" | 
|  | 6 | +	"os" | 
|  | 7 | +	"strings" | 
|  | 8 | +	"time" | 
|  | 9 | + | 
|  | 10 | +	"github.com/btcsuite/btcd/btcutil/hdkeychain" | 
|  | 11 | +	_ "github.com/btcsuite/btcwallet/walletdb/bdb" | 
|  | 12 | +	"github.com/lightninglabs/chantools/lnd" | 
|  | 13 | +	"github.com/lightningnetwork/lnd/aezeed" | 
|  | 14 | +	"github.com/lightningnetwork/lnd/keychain" | 
|  | 15 | +	"github.com/lightningnetwork/lnd/lnwallet" | 
|  | 16 | +	"github.com/lightningnetwork/lnd/lnwallet/btcwallet" | 
|  | 17 | +	"github.com/spf13/cobra" | 
|  | 18 | +) | 
|  | 19 | + | 
|  | 20 | +type createWalletCommand struct { | 
|  | 21 | +	WalletDBDir  string | 
|  | 22 | +	GenerateSeed bool | 
|  | 23 | + | 
|  | 24 | +	rootKey *rootKey | 
|  | 25 | +	cmd     *cobra.Command | 
|  | 26 | +} | 
|  | 27 | + | 
|  | 28 | +func newCreateWalletCommand() *cobra.Command { | 
|  | 29 | +	cc := &createWalletCommand{} | 
|  | 30 | +	cc.cmd = &cobra.Command{ | 
|  | 31 | +		Use: "createwallet", | 
|  | 32 | +		Short: "Create a new lnd compatible wallet.db file from an " + | 
|  | 33 | +			"existing seed or by generating a new one", | 
|  | 34 | +		Long: `Creates a new wallet that can be used with lnd or with  | 
|  | 35 | +chantools. The wallet can be created from an existing seed or a new one can be | 
|  | 36 | +generated (use --generateseed).`, | 
|  | 37 | +		Example: `chantools createwallet \ | 
|  | 38 | +	--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet`, | 
|  | 39 | +		RunE: cc.Execute, | 
|  | 40 | +	} | 
|  | 41 | +	cc.cmd.Flags().StringVar( | 
|  | 42 | +		&cc.WalletDBDir, "walletdbdir", "", "the folder to create the "+ | 
|  | 43 | +			"new wallet.db file in", | 
|  | 44 | +	) | 
|  | 45 | +	cc.cmd.Flags().BoolVar( | 
|  | 46 | +		&cc.GenerateSeed, "generateseed", false, "generate a new "+ | 
|  | 47 | +			"seed instead of using an existing one", | 
|  | 48 | +	) | 
|  | 49 | + | 
|  | 50 | +	cc.rootKey = newRootKey(cc.cmd, "creating the new wallet") | 
|  | 51 | + | 
|  | 52 | +	return cc.cmd | 
|  | 53 | +} | 
|  | 54 | + | 
|  | 55 | +func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error { | 
|  | 56 | +	var ( | 
|  | 57 | +		publicWalletPw  = lnwallet.DefaultPublicPassphrase | 
|  | 58 | +		privateWalletPw = lnwallet.DefaultPrivatePassphrase | 
|  | 59 | +		masterRootKey   *hdkeychain.ExtendedKey | 
|  | 60 | +		birthday        time.Time | 
|  | 61 | +		err             error | 
|  | 62 | +	) | 
|  | 63 | + | 
|  | 64 | +	// Check that we have a wallet DB. | 
|  | 65 | +	if c.WalletDBDir == "" { | 
|  | 66 | +		return fmt.Errorf("wallet DB directory is required") | 
|  | 67 | +	} | 
|  | 68 | + | 
|  | 69 | +	// Make sure the directory (and parents) exists. | 
|  | 70 | +	if err := os.MkdirAll(c.WalletDBDir, 0700); err != nil { | 
|  | 71 | +		return fmt.Errorf("error creating wallet DB directory '%s': %w", | 
|  | 72 | +			c.WalletDBDir, err) | 
|  | 73 | +	} | 
|  | 74 | + | 
|  | 75 | +	// Check if we should create a new seed or read if from the console or | 
|  | 76 | +	// environment. | 
|  | 77 | +	if c.GenerateSeed { | 
|  | 78 | +		fmt.Printf("Generating new lnd compatible aezeed...\n") | 
|  | 79 | +		seed, err := aezeed.New( | 
|  | 80 | +			keychain.KeyDerivationVersionTaproot, nil, time.Now(), | 
|  | 81 | +		) | 
|  | 82 | +		if err != nil { | 
|  | 83 | +			return fmt.Errorf("error creating new seed: %w", err) | 
|  | 84 | +		} | 
|  | 85 | +		birthday = seed.BirthdayTime() | 
|  | 86 | + | 
|  | 87 | +		// Derive the master extended key from the seed. | 
|  | 88 | +		masterRootKey, err = hdkeychain.NewMaster( | 
|  | 89 | +			seed.Entropy[:], chainParams, | 
|  | 90 | +		) | 
|  | 91 | +		if err != nil { | 
|  | 92 | +			return fmt.Errorf("failed to derive master extended "+ | 
|  | 93 | +				"key: %w", err) | 
|  | 94 | +		} | 
|  | 95 | + | 
|  | 96 | +		passphrase, err := lnd.ReadPassphrase("shouldn't use") | 
|  | 97 | +		if err != nil { | 
|  | 98 | +			return fmt.Errorf("error reading passphrase: %w", err) | 
|  | 99 | +		} | 
|  | 100 | + | 
|  | 101 | +		mnemonic, err := seed.ToMnemonic(passphrase) | 
|  | 102 | +		if err != nil { | 
|  | 103 | +			return fmt.Errorf("error converting seed to "+ | 
|  | 104 | +				"mnemonic: %w", err) | 
|  | 105 | +		} | 
|  | 106 | + | 
|  | 107 | +		fmt.Println("Generated new seed") | 
|  | 108 | +		printCipherSeedWords(mnemonic[:]) | 
|  | 109 | +	} else { | 
|  | 110 | +		masterRootKey, birthday, err = c.rootKey.readWithBirthday() | 
|  | 111 | +		if err != nil { | 
|  | 112 | +			return err | 
|  | 113 | +		} | 
|  | 114 | +	} | 
|  | 115 | + | 
|  | 116 | +	// To automate things with chantools, we also offer reading the wallet | 
|  | 117 | +	// password from environment variables. | 
|  | 118 | +	pw := []byte(strings.TrimSpace(os.Getenv(lnd.PasswordEnvName))) | 
|  | 119 | + | 
|  | 120 | +	// Because we cannot differentiate between an empty and a non-existent | 
|  | 121 | +	// environment variable, we need a special character that indicates that | 
|  | 122 | +	// no password should be used. We use a single dash (-) for that as that | 
|  | 123 | +	// would be too short for an explicit password anyway. | 
|  | 124 | +	switch { | 
|  | 125 | +	// The user indicated in the environment variable that no passphrase | 
|  | 126 | +	// should be used. We don't set any value. | 
|  | 127 | +	case string(pw) == "-": | 
|  | 128 | + | 
|  | 129 | +	// The environment variable didn't contain anything, we'll read the | 
|  | 130 | +	// passphrase from the terminal. | 
|  | 131 | +	case len(pw) == 0: | 
|  | 132 | +		fmt.Printf("\n\nThe wallet password is used to encrypt the " + | 
|  | 133 | +			"wallet.db file itself and is unrelated to the seed.\n") | 
|  | 134 | +		pw, err = lnd.PasswordFromConsole("Input new wallet password: ") | 
|  | 135 | +		if err != nil { | 
|  | 136 | +			return err | 
|  | 137 | +		} | 
|  | 138 | +		pw2, err := lnd.PasswordFromConsole( | 
|  | 139 | +			"Confirm new wallet password: ", | 
|  | 140 | +		) | 
|  | 141 | +		if err != nil { | 
|  | 142 | +			return err | 
|  | 143 | +		} | 
|  | 144 | + | 
|  | 145 | +		if !bytes.Equal(pw, pw2) { | 
|  | 146 | +			return fmt.Errorf("passwords don't match") | 
|  | 147 | +		} | 
|  | 148 | + | 
|  | 149 | +		if len(pw) > 0 { | 
|  | 150 | +			publicWalletPw = pw | 
|  | 151 | +			privateWalletPw = pw | 
|  | 152 | +		} | 
|  | 153 | + | 
|  | 154 | +	// There was a password in the environment, just use it directly. | 
|  | 155 | +	default: | 
|  | 156 | +		publicWalletPw = pw | 
|  | 157 | +		privateWalletPw = pw | 
|  | 158 | +	} | 
|  | 159 | + | 
|  | 160 | +	// Try to create the wallet. | 
|  | 161 | +	loader, err := btcwallet.NewWalletLoader( | 
|  | 162 | +		chainParams, 0, btcwallet.LoaderWithLocalWalletDB( | 
|  | 163 | +			c.WalletDBDir, true, 0, | 
|  | 164 | +		), | 
|  | 165 | +	) | 
|  | 166 | +	if err != nil { | 
|  | 167 | +		return fmt.Errorf("error creating wallet loader: %w", err) | 
|  | 168 | +	} | 
|  | 169 | + | 
|  | 170 | +	_, err = loader.CreateNewWalletExtendedKey( | 
|  | 171 | +		publicWalletPw, privateWalletPw, masterRootKey, birthday, | 
|  | 172 | +	) | 
|  | 173 | +	if err != nil { | 
|  | 174 | +		return fmt.Errorf("error creating new wallet: %w", err) | 
|  | 175 | +	} | 
|  | 176 | + | 
|  | 177 | +	if err := loader.UnloadWallet(); err != nil { | 
|  | 178 | +		return fmt.Errorf("error unloading wallet: %w", err) | 
|  | 179 | +	} | 
|  | 180 | + | 
|  | 181 | +	fmt.Printf("Wallet created successfully at %v\n", c.WalletDBDir) | 
|  | 182 | + | 
|  | 183 | +	return nil | 
|  | 184 | +} | 
|  | 185 | + | 
|  | 186 | +func printCipherSeedWords(mnemonicWords []string) { | 
|  | 187 | +	fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " + | 
|  | 188 | +		"RESTORE THE WALLET!!!") | 
|  | 189 | +	fmt.Println() | 
|  | 190 | + | 
|  | 191 | +	fmt.Println("---------------BEGIN LND CIPHER SEED---------------") | 
|  | 192 | + | 
|  | 193 | +	numCols := 4 | 
|  | 194 | +	colWords := monoWidthColumns(mnemonicWords, numCols) | 
|  | 195 | +	for i := 0; i < len(colWords); i += numCols { | 
|  | 196 | +		fmt.Printf("%2d. %3s  %2d. %3s  %2d. %3s  %2d. %3s\n", | 
|  | 197 | +			i+1, colWords[i], i+2, colWords[i+1], i+3, | 
|  | 198 | +			colWords[i+2], i+4, colWords[i+3]) | 
|  | 199 | +	} | 
|  | 200 | + | 
|  | 201 | +	fmt.Println("---------------END LND CIPHER SEED-----------------") | 
|  | 202 | + | 
|  | 203 | +	fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " + | 
|  | 204 | +		"RESTORE THE WALLET!!!") | 
|  | 205 | +} | 
|  | 206 | + | 
|  | 207 | +// monoWidthColumns takes a set of words, and the number of desired columns, | 
|  | 208 | +// and returns a new set of words that have had white space appended to the | 
|  | 209 | +// word in order to create a mono-width column. | 
|  | 210 | +func monoWidthColumns(words []string, ncols int) []string { | 
|  | 211 | +	// Determine max size of words in each column. | 
|  | 212 | +	colWidths := make([]int, ncols) | 
|  | 213 | +	for i, word := range words { | 
|  | 214 | +		col := i % ncols | 
|  | 215 | +		curWidth := colWidths[col] | 
|  | 216 | +		if len(word) > curWidth { | 
|  | 217 | +			colWidths[col] = len(word) | 
|  | 218 | +		} | 
|  | 219 | +	} | 
|  | 220 | + | 
|  | 221 | +	// Append whitespace to each word to make columns mono-width. | 
|  | 222 | +	finalWords := make([]string, len(words)) | 
|  | 223 | +	for i, word := range words { | 
|  | 224 | +		col := i % ncols | 
|  | 225 | +		width := colWidths[col] | 
|  | 226 | + | 
|  | 227 | +		diff := width - len(word) | 
|  | 228 | +		finalWords[i] = word + strings.Repeat(" ", diff) | 
|  | 229 | +	} | 
|  | 230 | + | 
|  | 231 | +	return finalWords | 
|  | 232 | +} | 
0 commit comments