github.com/decred/dcrlnd@v0.7.6/cmd/dcrlncli/cmd_walletunlocker.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/decred/dcrlnd/lncfg"
    14  	"github.com/decred/dcrlnd/lnrpc"
    15  	"github.com/decred/dcrlnd/lnrpc/walletrpc"
    16  	"github.com/decred/dcrlnd/walletunlocker"
    17  	"github.com/matheusd/protobuf-hex-display/jsonpb"
    18  	"github.com/urfave/cli"
    19  )
    20  
    21  var (
    22  	statelessInitFlag = cli.BoolFlag{
    23  		Name: "stateless_init",
    24  		Usage: "do not create any macaroon files in the file " +
    25  			"system of the daemon",
    26  	}
    27  	saveToFlag = cli.StringFlag{
    28  		Name:  "save_to",
    29  		Usage: "save returned admin macaroon to this file",
    30  	}
    31  )
    32  
    33  var createCommand = cli.Command{
    34  	Name:     "create",
    35  	Category: "Startup",
    36  	Usage:    "Initialize a wallet when starting dcrlnd for the first time.",
    37  	Description: `
    38  	The create command is used to initialize an dcrlnd wallet from scratch for
    39  	the very first time. This is interactive command with one required
    40  	argument (the password), and one optional argument (the mnemonic
    41  	passphrase).
    42  
    43  	The first argument (the password) is required and MUST be greater than
    44  	8 characters. This will be used to encrypt the wallet within dcrlnd. This
    45  	MUST be remembered as it will be required to fully start up the daemon.
    46  
    47  	The second argument is an optional 24-word mnemonic derived from BIP
    48  	39. If provided, then the internal wallet will use the seed derived
    49  	from this mnemonic to generate all keys.
    50  
    51  	This command returns a 24-word seed in the scenario that NO mnemonic
    52  	was provided by the user. This should be written down as it can be used
    53  	to potentially recover all on-chain funds, and most off-chain funds as
    54  	well.
    55  
    56  	If the --stateless_init flag is set, no macaroon files are created by
    57  	the daemon. Instead, the binary serialized admin macaroon is returned
    58  	in the answer. This answer MUST be stored somewhere, otherwise all
    59  	access to the RPC server will be lost and the wallet must be recreated
    60  	to re-gain access.
    61  	If the --save_to parameter is set, the macaroon is saved to this file,
    62  	otherwise it is printed to standard out.
    63  
    64  	Finally, it's also possible to use this command and a set of static
    65  	channel backups to trigger a recover attempt for the provided Static
    66  	Channel Backups. Only one of the three parameters will be accepted. See
    67  	the 'restorechanbackup' command for further details w.r.t the format
    68  	accepted.
    69  	`,
    70  	Flags: []cli.Flag{
    71  		cli.StringFlag{
    72  			Name: "single_backup",
    73  			Usage: "A hex encoded single channel backup obtained " +
    74  				"from exportchanbackup",
    75  		},
    76  		cli.StringFlag{
    77  			Name: "multi_backup",
    78  			Usage: "A hex encoded multi-channel backup obtained " +
    79  				"from exportchanbackup",
    80  		},
    81  		cli.StringFlag{
    82  			Name:  "multi_file",
    83  			Usage: "The path to a multi-channel back up file",
    84  		},
    85  		statelessInitFlag,
    86  		saveToFlag,
    87  	},
    88  	Action: actionDecorator(create),
    89  }
    90  
    91  // monowidthColumns takes a set of words, and the number of desired columns,
    92  // and returns a new set of words that have had white space appended to the
    93  // word in order to create a mono-width column.
    94  func monowidthColumns(words []string, ncols int) []string {
    95  	// Determine max size of words in each column.
    96  	colWidths := make([]int, ncols)
    97  	for i, word := range words {
    98  		col := i % ncols
    99  		curWidth := colWidths[col]
   100  		if len(word) > curWidth {
   101  			colWidths[col] = len(word)
   102  		}
   103  	}
   104  
   105  	// Append whitespace to each word to make columns mono-width.
   106  	finalWords := make([]string, len(words))
   107  	for i, word := range words {
   108  		col := i % ncols
   109  		width := colWidths[col]
   110  
   111  		diff := width - len(word)
   112  		finalWords[i] = word + strings.Repeat(" ", diff)
   113  	}
   114  
   115  	return finalWords
   116  }
   117  
   118  func create(ctx *cli.Context) error {
   119  	ctxc := getContext()
   120  	client, cleanUp := getWalletUnlockerClient(ctx)
   121  	defer cleanUp()
   122  
   123  	var (
   124  		chanBackups *lnrpc.ChanBackupSnapshot
   125  
   126  		// We use var restoreSCB to track if we will be including an SCB
   127  		// recovery in the init wallet request.
   128  		restoreSCB = false
   129  	)
   130  
   131  	backups, err := parseChanBackups(ctx)
   132  
   133  	// We'll check to see if the user provided any static channel backups (SCB),
   134  	// if so, we will warn the user that SCB recovery closes all open channels
   135  	// and ask them to confirm their intention.
   136  	// If the user agrees, we'll add the SCB recovery onto the final init wallet
   137  	// request.
   138  	switch {
   139  	// parseChanBackups returns an errMissingBackup error (which we ignore) if
   140  	// the user did not request a SCB recovery.
   141  	case err == errMissingChanBackup:
   142  
   143  	// Passed an invalid channel backup file.
   144  	case err != nil:
   145  		return fmt.Errorf("unable to parse chan backups: %v", err)
   146  
   147  	// We have an SCB recovery option with a valid backup file.
   148  	default:
   149  
   150  	warningLoop:
   151  		for {
   152  
   153  			fmt.Println()
   154  			fmt.Printf("WARNING: You are attempting to restore from a " +
   155  				"static channel backup (SCB) file.\nThis action will CLOSE " +
   156  				"all currently open channels, and you will pay on-chain fees." +
   157  				"\n\nAre you sure you want to recover funds from a" +
   158  				" static channel backup? (Enter y/n): ")
   159  
   160  			reader := bufio.NewReader(os.Stdin)
   161  			answer, err := reader.ReadString('\n')
   162  			if err != nil {
   163  				return err
   164  			}
   165  
   166  			answer = strings.TrimSpace(answer)
   167  			answer = strings.ToLower(answer)
   168  
   169  			switch answer {
   170  			case "y":
   171  				restoreSCB = true
   172  				break warningLoop
   173  			case "n":
   174  				fmt.Println("Aborting SCB recovery")
   175  				return nil
   176  			}
   177  		}
   178  	}
   179  
   180  	// Proceed with SCB recovery.
   181  	if restoreSCB {
   182  		fmt.Println("Static Channel Backup (SCB) recovery selected!")
   183  		if backups != nil {
   184  			switch {
   185  			case backups.GetChanBackups() != nil:
   186  				singleBackup := backups.GetChanBackups()
   187  				chanBackups = &lnrpc.ChanBackupSnapshot{
   188  					SingleChanBackups: singleBackup,
   189  				}
   190  
   191  			case backups.GetMultiChanBackup() != nil:
   192  				multiBackup := backups.GetMultiChanBackup()
   193  				chanBackups = &lnrpc.ChanBackupSnapshot{
   194  					MultiChanBackup: &lnrpc.MultiChanBackup{
   195  						MultiChanBackup: multiBackup,
   196  					},
   197  				}
   198  			}
   199  		}
   200  	}
   201  
   202  	// Should the daemon be initialized stateless? Then we expect an answer
   203  	// with the admin macaroon later. Because the --save_to is related to
   204  	// stateless init, it doesn't make sense to be set on its own.
   205  	statelessInit := ctx.Bool(statelessInitFlag.Name)
   206  	if !statelessInit && ctx.IsSet(saveToFlag.Name) {
   207  		return fmt.Errorf("cannot set save_to parameter without " +
   208  			"stateless_init")
   209  	}
   210  
   211  	walletPassword, err := capturePassword(
   212  		"Input wallet password: ", false, walletunlocker.ValidatePassword,
   213  	)
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	// Next, we'll see if the user has 24-word mnemonic they want to use to
   219  	// derive a seed within the wallet or if they want to specify an
   220  	// extended master root key (xprv) directly.
   221  	var (
   222  		hasMnemonic bool
   223  		hasXprv     bool
   224  	)
   225  
   226  mnemonicCheck:
   227  	for {
   228  		fmt.Println()
   229  		fmt.Printf("Do you have an existing cipher seed " +
   230  			"mnemonic or extended master root key you want to " +
   231  			"use?\nEnter 'y' to use an existing cipher seed " +
   232  			"mnemonic " +
   233  			"\nor 'n' to create a new seed (Enter y/n): ")
   234  
   235  		reader := bufio.NewReader(os.Stdin)
   236  		answer, err := reader.ReadString('\n')
   237  		if err != nil {
   238  			return err
   239  		}
   240  
   241  		fmt.Println()
   242  
   243  		answer = strings.TrimSpace(answer)
   244  		answer = strings.ToLower(answer)
   245  
   246  		switch answer {
   247  		case "y":
   248  			hasMnemonic = true
   249  			break mnemonicCheck
   250  
   251  		case "n":
   252  			break mnemonicCheck
   253  		}
   254  	}
   255  
   256  	// If the user *does* have an existing seed or root key they want to
   257  	// use, then we'll read that in directly from the terminal.
   258  	var (
   259  		cipherSeedMnemonic      []string
   260  		aezeedPass              []byte
   261  		extendedRootKey         string
   262  		extendedRootKeyBirthday uint64
   263  		recoveryWindow          int32
   264  	)
   265  	switch {
   266  	// Use an existing cipher seed mnemonic in the aezeed format.
   267  	case hasMnemonic:
   268  		// We'll now prompt the user to enter in their 24-word
   269  		// mnemonic.
   270  		fmt.Printf("Input your 24-word mnemonic separated by spaces: ")
   271  		reader := bufio.NewReader(os.Stdin)
   272  		mnemonic, err := reader.ReadString('\n')
   273  		if err != nil {
   274  			return err
   275  		}
   276  
   277  		// We'll trim off extra spaces, and ensure the mnemonic is all
   278  		// lower case, then populate our request.
   279  		mnemonic = strings.TrimSpace(mnemonic)
   280  		mnemonic = strings.ToLower(mnemonic)
   281  
   282  		cipherSeedMnemonic = strings.Split(mnemonic, " ")
   283  
   284  		fmt.Println()
   285  
   286  		if len(cipherSeedMnemonic) != 24 {
   287  			return fmt.Errorf("wrong cipher seed mnemonic "+
   288  				"length: got %v words, expecting %v words",
   289  				len(cipherSeedMnemonic), 24)
   290  		}
   291  
   292  		// Additionally, the user may have a passphrase, that will also
   293  		// need to be provided so the daemon can properly decipher the
   294  		// cipher seed.
   295  		aezeedPass, err = readPassword("Input your cipher seed " +
   296  			"passphrase (press enter if your seed doesn't have a " +
   297  			"passphrase): ")
   298  		if err != nil {
   299  			return err
   300  		}
   301  
   302  		recoveryWindow, err = askRecoveryWindow()
   303  		if err != nil {
   304  			return err
   305  		}
   306  
   307  	// Use an existing extended master root key to create the wallet.
   308  	case hasXprv:
   309  		// We'll now prompt the user to enter in their extended master
   310  		// root key.
   311  		fmt.Printf("Input your extended master root key (usually " +
   312  			"starting with xprv... on mainnet): ")
   313  		reader := bufio.NewReader(os.Stdin)
   314  		extendedRootKey, err = reader.ReadString('\n')
   315  		if err != nil {
   316  			return err
   317  		}
   318  		extendedRootKey = strings.TrimSpace(extendedRootKey)
   319  
   320  		extendedRootKeyBirthday, err = askBirthdayTimestamp()
   321  		if err != nil {
   322  			return err
   323  		}
   324  
   325  		recoveryWindow, err = askRecoveryWindow()
   326  		if err != nil {
   327  			return err
   328  		}
   329  
   330  	// Neither a seed nor a master root key was specified, the user wants
   331  	// to create a new seed.
   332  	default:
   333  		// Otherwise, if the user doesn't have a mnemonic that they
   334  		// want to use, we'll generate a fresh one with the GenSeed
   335  		// command.
   336  		fmt.Println("Your cipher seed can optionally be encrypted.")
   337  
   338  		instruction := "Input your passphrase if you wish to encrypt it " +
   339  			"(or press enter to proceed without a cipher seed " +
   340  			"passphrase): "
   341  		aezeedPass, err = capturePassword(
   342  			instruction, true, func(_ []byte) error { return nil },
   343  		)
   344  		if err != nil {
   345  			return err
   346  		}
   347  
   348  		fmt.Println()
   349  		fmt.Println("Generating fresh cipher seed...")
   350  		fmt.Println()
   351  
   352  		genSeedReq := &lnrpc.GenSeedRequest{
   353  			AezeedPassphrase: aezeedPass,
   354  		}
   355  		seedResp, err := client.GenSeed(ctxc, genSeedReq)
   356  		if err != nil {
   357  			return fmt.Errorf("unable to generate seed: %v", err)
   358  		}
   359  
   360  		cipherSeedMnemonic = seedResp.CipherSeedMnemonic
   361  	}
   362  
   363  	// Before we initialize the wallet, we'll display the cipher seed to
   364  	// the user so they can write it down.
   365  	if len(cipherSeedMnemonic) > 0 {
   366  		printCipherSeedWords(cipherSeedMnemonic)
   367  	}
   368  
   369  	// With either the user's prior cipher seed, or a newly generated one,
   370  	// we'll go ahead and initialize the wallet.
   371  	req := &lnrpc.InitWalletRequest{
   372  		WalletPassword:                     walletPassword,
   373  		CipherSeedMnemonic:                 cipherSeedMnemonic,
   374  		AezeedPassphrase:                   aezeedPass,
   375  		ExtendedMasterKey:                  extendedRootKey,
   376  		ExtendedMasterKeyBirthdayTimestamp: extendedRootKeyBirthday,
   377  		RecoveryWindow:                     recoveryWindow,
   378  		ChannelBackups:                     chanBackups,
   379  		StatelessInit:                      statelessInit,
   380  	}
   381  	response, err := client.InitWallet(ctxc, req)
   382  	if err != nil {
   383  		return err
   384  	}
   385  
   386  	fmt.Println("\ndcrlnd successfully initialized!")
   387  
   388  	if statelessInit {
   389  		return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
   390  	}
   391  
   392  	return nil
   393  }
   394  
   395  // capturePassword returns a password value that has been entered twice by the
   396  // user, to ensure that the user knows what password they have entered. The user
   397  // will be prompted to retry until the passwords match. If the optional param is
   398  // true, the function may return an empty byte array if the user opts against
   399  // using a password.
   400  func capturePassword(instruction string, optional bool,
   401  	validate func([]byte) error) ([]byte, error) {
   402  
   403  	for {
   404  		password, err := readPassword(instruction)
   405  		if err != nil {
   406  			return nil, err
   407  		}
   408  
   409  		// Do not require users to repeat password if
   410  		// it is optional and they are not using one.
   411  		if len(password) == 0 && optional {
   412  			return nil, nil
   413  		}
   414  
   415  		// If the password provided is not valid, restart
   416  		// password capture process from the beginning.
   417  		if err := validate(password); err != nil {
   418  			fmt.Println(err.Error())
   419  			fmt.Println()
   420  			continue
   421  		}
   422  
   423  		passwordConfirmed, err := readPassword("Confirm password: ")
   424  		if err != nil {
   425  			return nil, err
   426  		}
   427  
   428  		if bytes.Equal(password, passwordConfirmed) {
   429  			return password, nil
   430  		}
   431  
   432  		fmt.Println("Passwords don't match, please try again")
   433  		fmt.Println()
   434  	}
   435  }
   436  
   437  var unlockCommand = cli.Command{
   438  	Name:     "unlock",
   439  	Category: "Startup",
   440  	Usage:    "Unlock an encrypted wallet at startup.",
   441  	Description: `
   442  	The unlock command is used to decrypt dcrlnd's wallet state in order to
   443  	start up. This command MUST be run after booting up dcrlnd before it's
   444  	able to carry out its duties. An exception is if a user is running with
   445  	--noseedbackup, then a default passphrase will be used.
   446  
   447  	If the --stateless_init flag is set, no macaroon files are created by
   448  	the daemon. This should be set for every unlock if the daemon was
   449  	initially initialized stateless. Otherwise the daemon will create
   450  	unencrypted macaroon files which could leak information to the system
   451  	that the daemon runs on.
   452  	`,
   453  	Flags: []cli.Flag{
   454  		cli.IntFlag{
   455  			Name: "recovery_window",
   456  			Usage: "Address lookahead to resume recovery rescan, " +
   457  				"value should be non-zero --  To recover all " +
   458  				"funds, this should be greater than the " +
   459  				"maximum number of consecutive, unused " +
   460  				"addresses ever generated by the wallet.",
   461  		},
   462  		cli.BoolFlag{
   463  			Name: "stdin",
   464  			Usage: "read password from standard input instead of " +
   465  				"prompting for it. THIS IS CONSIDERED TO " +
   466  				"BE DANGEROUS if the password is located in " +
   467  				"a file that can be read by another user. " +
   468  				"This flag should only be used in " +
   469  				"combination with some sort of password " +
   470  				"manager or secrets vault.",
   471  		},
   472  		statelessInitFlag,
   473  	},
   474  	Action: actionDecorator(unlock),
   475  }
   476  
   477  func unlock(ctx *cli.Context) error {
   478  	ctxc := getContext()
   479  	client, cleanUp := getWalletUnlockerClient(ctx)
   480  	defer cleanUp()
   481  
   482  	var (
   483  		pw  []byte
   484  		err error
   485  	)
   486  	switch {
   487  	// Read the password from standard in as if it were a file. This should
   488  	// only be used if the password is piped into lncli from some sort of
   489  	// password manager. If the user types the password instead, it will be
   490  	// echoed in the console.
   491  	case ctx.IsSet("stdin"):
   492  		reader := bufio.NewReader(os.Stdin)
   493  		pw, err = reader.ReadBytes('\n')
   494  
   495  		// Remove carriage return and newline characters.
   496  		pw = bytes.Trim(pw, "\r\n")
   497  
   498  	// Read the password from a terminal by default. This requires the
   499  	// terminal to be a real tty and will fail if a string is piped into
   500  	// lncli.
   501  	default:
   502  		pw, err = readPassword("Input wallet password: ")
   503  	}
   504  	if err != nil {
   505  		return err
   506  	}
   507  
   508  	args := ctx.Args()
   509  
   510  	// Parse the optional recovery window if it is specified. By default,
   511  	// the recovery window will be 0, indicating no lookahead should be
   512  	// used.
   513  	var recoveryWindow int32
   514  	switch {
   515  	case ctx.IsSet("recovery_window"):
   516  		recoveryWindow = int32(ctx.Int64("recovery_window"))
   517  	case args.Present():
   518  		window, err := strconv.ParseInt(args.First(), 10, 64)
   519  		if err != nil {
   520  			return err
   521  		}
   522  		recoveryWindow = int32(window)
   523  	}
   524  
   525  	req := &lnrpc.UnlockWalletRequest{
   526  		WalletPassword: pw,
   527  		RecoveryWindow: recoveryWindow,
   528  		StatelessInit:  ctx.Bool(statelessInitFlag.Name),
   529  	}
   530  	_, err = client.UnlockWallet(ctxc, req)
   531  	if err != nil {
   532  		return err
   533  	}
   534  
   535  	fmt.Println("\ndcrlnd successfully unlocked!")
   536  
   537  	// TODO(roasbeef): add ability to accept hex single and multi backups
   538  
   539  	return nil
   540  }
   541  
   542  var changePasswordCommand = cli.Command{
   543  	Name:     "changepassword",
   544  	Category: "Startup",
   545  	Usage:    "Change an encrypted wallet's password at startup.",
   546  	Description: `
   547  	The changepassword command is used to Change dcrlnd's encrypted wallet's
   548  	password. It will automatically unlock the daemon if the password change
   549  	is successful.
   550  
   551  	If one did not specify a password for their wallet (running dcrlnd with
   552  	'--noseedbackup'), one must restart their daemon without
   553  	'--noseedbackup' and use this command. The "current password" field
   554  	should be left empty.
   555  
   556  	If the daemon was originally initialized stateless, then the
   557  	--stateless_init flag needs to be set for the change password request
   558  	as well! Otherwise the daemon will generate unencrypted macaroon files
   559  	in its file system again and possibly leak sensitive information.
   560  	Changing the password will by default not change the macaroon root key
   561  	(just re-encrypt the macaroon database with the new password). So all
   562  	macaroons will still be valid.
   563  	If one wants to make sure that all previously created macaroons are
   564  	invalidated, a new macaroon root key can be generated by using the
   565  	--new_mac_root_key flag.
   566  
   567  	After a successful password change with the --stateless_init flag set,
   568  	the current or new admin macaroon is returned binary serialized in the
   569  	answer. This answer MUST then be stored somewhere, otherwise
   570  	all access to the RPC server will be lost and the wallet must be re-
   571  	created to re-gain access. If the --save_to parameter is set, the
   572  	macaroon is saved to this file, otherwise it is printed to standard out.
   573  	`,
   574  	Flags: []cli.Flag{
   575  		statelessInitFlag,
   576  		saveToFlag,
   577  		cli.BoolFlag{
   578  			Name: "new_mac_root_key",
   579  			Usage: "rotate the macaroon root key resulting in " +
   580  				"all previously created macaroons to be " +
   581  				"invalidated",
   582  		},
   583  	},
   584  	Action: actionDecorator(changePassword),
   585  }
   586  
   587  func changePassword(ctx *cli.Context) error {
   588  	ctxc := getContext()
   589  	client, cleanUp := getWalletUnlockerClient(ctx)
   590  	defer cleanUp()
   591  
   592  	currentPw, err := readPassword("Input current wallet password: ")
   593  	if err != nil {
   594  		return err
   595  	}
   596  
   597  	newPw, err := readPassword("Input new wallet password: ")
   598  	if err != nil {
   599  		return err
   600  	}
   601  
   602  	confirmPw, err := readPassword("Confirm new wallet password: ")
   603  	if err != nil {
   604  		return err
   605  	}
   606  
   607  	if !bytes.Equal(newPw, confirmPw) {
   608  		return fmt.Errorf("passwords don't match")
   609  	}
   610  
   611  	// Should the daemon be initialized stateless? Then we expect an answer
   612  	// with the admin macaroon later. Because the --save_to is related to
   613  	// stateless init, it doesn't make sense to be set on its own.
   614  	statelessInit := ctx.Bool(statelessInitFlag.Name)
   615  	if !statelessInit && ctx.IsSet(saveToFlag.Name) {
   616  		return fmt.Errorf("cannot set save_to parameter without " +
   617  			"stateless_init")
   618  	}
   619  
   620  	req := &lnrpc.ChangePasswordRequest{
   621  		CurrentPassword:    currentPw,
   622  		NewPassword:        newPw,
   623  		StatelessInit:      statelessInit,
   624  		NewMacaroonRootKey: ctx.Bool("new_mac_root_key"),
   625  	}
   626  
   627  	response, err := client.ChangePassword(ctxc, req)
   628  	if err != nil {
   629  		return err
   630  	}
   631  
   632  	if statelessInit {
   633  		return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
   634  	}
   635  
   636  	return nil
   637  }
   638  
   639  var createWatchOnlyCommand = cli.Command{
   640  	Name:      "createwatchonly",
   641  	Category:  "Startup",
   642  	ArgsUsage: "accounts-json-file",
   643  	Usage: "Initialize a watch-only wallet after starting lnd for the " +
   644  		"first time.",
   645  	Description: `
   646  	The create command is used to initialize an lnd wallet from scratch for
   647  	the very first time, in watch-only mode. Watch-only means, there will be
   648  	no private keys in lnd's wallet. This is only useful in combination with
   649  	a remote signer or when lnd should be used as an on-chain wallet with
   650  	PSBT interaction only.
   651  
   652  	This is an interactive command that takes a JSON file as its first and
   653  	only argument. The JSON is in the same format as the output of the
   654  	'lncli wallet accounts list' command. This makes it easy to initialize
   655  	the remote signer with the seed, then export the extended public account
   656  	keys (xpubs) to import the watch-only wallet.
   657  
   658  	Example JSON (non-mandatory or ignored fields are omitted):
   659  	{
   660  	    "accounts": [
   661  		{
   662  		    "extended_public_key": "upub5Eep7....",
   663  		    "derivation_path": "m/49'/0'/0'"
   664  		},
   665  		{
   666  		    "extended_public_key": "vpub5ZU1PH...",
   667  		    "derivation_path": "m/84'/0'/0'"
   668  		},
   669  		{
   670  		    "extended_public_key": "tpubDDXFH...",
   671  		    "derivation_path": "m/1017'/1'/0'"
   672  		},
   673  	        ...
   674  		{
   675  		    "extended_public_key": "tpubDDXFH...",
   676  		    "derivation_path": "m/1017'/1'/9'"
   677  		}
   678  	   ]
   679  	}
   680  
   681  	There must be an account for each of the existing key families that lnd
   682  	uses internally (currently 0-9, see keychain/derivation.go).
   683  
   684  	Read the documentation under docs/remote-signing.md for more information
   685  	on how to set up a remote signing node over RPC.
   686  	`,
   687  	Action: actionDecorator(createWatchOnly),
   688  }
   689  
   690  func createWatchOnly(ctx *cli.Context) error {
   691  	ctxc := getContext()
   692  	client, cleanUp := getWalletUnlockerClient(ctx)
   693  	defer cleanUp()
   694  
   695  	if ctx.NArg() != 1 {
   696  		return cli.ShowCommandHelp(ctx, "createwatchonly")
   697  	}
   698  
   699  	jsonFile := lncfg.CleanAndExpandPath(ctx.Args().First())
   700  	jsonBytes, err := ioutil.ReadFile(jsonFile)
   701  	if err != nil {
   702  		return fmt.Errorf("error reading JSON from file %v: %v",
   703  			jsonFile, err)
   704  	}
   705  
   706  	jsonAccts := &walletrpc.ListAccountsResponse{}
   707  	err = jsonpb.Unmarshal(bytes.NewReader(jsonBytes), jsonAccts)
   708  	if err != nil {
   709  		return fmt.Errorf("error parsing JSON: %v", err)
   710  	}
   711  	if len(jsonAccts.Accounts) == 0 {
   712  		return fmt.Errorf("cannot import empty account list")
   713  	}
   714  
   715  	walletPassword, err := capturePassword(
   716  		"Input wallet password: ", false,
   717  		walletunlocker.ValidatePassword,
   718  	)
   719  	if err != nil {
   720  		return err
   721  	}
   722  
   723  	extendedRootKeyBirthday, err := askBirthdayTimestamp()
   724  	if err != nil {
   725  		return err
   726  	}
   727  
   728  	recoveryWindow, err := askRecoveryWindow()
   729  	if err != nil {
   730  		return err
   731  	}
   732  
   733  	rpcAccounts, err := walletrpc.AccountsToWatchOnly(jsonAccts.Accounts)
   734  	if err != nil {
   735  		return err
   736  	}
   737  
   738  	rpcResp := &lnrpc.WatchOnly{
   739  		MasterKeyBirthdayTimestamp: extendedRootKeyBirthday,
   740  		Accounts:                   rpcAccounts,
   741  	}
   742  
   743  	// We assume that all accounts were exported from the same master root
   744  	// key. So if one is set, we just forward that. If other accounts should
   745  	// be watched later on, they should be imported into the watch-only
   746  	// node, that then also forwards the import request to the remote
   747  	// signer.
   748  	for _, acct := range jsonAccts.Accounts {
   749  		if len(acct.MasterKeyFingerprint) > 0 {
   750  			rpcResp.MasterKeyFingerprint = acct.MasterKeyFingerprint
   751  		}
   752  	}
   753  
   754  	_, err = client.InitWallet(ctxc, &lnrpc.InitWalletRequest{
   755  		WalletPassword: walletPassword,
   756  		WatchOnly:      rpcResp,
   757  		RecoveryWindow: recoveryWindow,
   758  	})
   759  	return err
   760  }
   761  
   762  // storeOrPrintAdminMac either stores the admin macaroon to a file specified or
   763  // prints it to standard out, depending on the user flags set.
   764  func storeOrPrintAdminMac(ctx *cli.Context, adminMac []byte) error {
   765  	// The user specified the optional --save_to parameter. We'll save the
   766  	// macaroon to that file.
   767  	if ctx.IsSet("save_to") {
   768  		macSavePath := lncfg.CleanAndExpandPath(ctx.String("save_to"))
   769  		err := ioutil.WriteFile(macSavePath, adminMac, 0644)
   770  		if err != nil {
   771  			_ = os.Remove(macSavePath)
   772  			return err
   773  		}
   774  		fmt.Printf("Admin macaroon saved to %s\n", macSavePath)
   775  		return nil
   776  	}
   777  
   778  	// Otherwise we just print it. The user MUST store this macaroon
   779  	// somewhere so we either save it to a provided file path or just print
   780  	// it to standard output.
   781  	fmt.Printf("Admin macaroon: %s\n", hex.EncodeToString(adminMac))
   782  	return nil
   783  }
   784  
   785  func askRecoveryWindow() (int32, error) {
   786  	for {
   787  		fmt.Println()
   788  		fmt.Printf("Input an optional address look-ahead used to scan "+
   789  			"for used keys (default %d): ", defaultRecoveryWindow)
   790  
   791  		reader := bufio.NewReader(os.Stdin)
   792  		answer, err := reader.ReadString('\n')
   793  		if err != nil {
   794  			return 0, err
   795  		}
   796  
   797  		fmt.Println()
   798  
   799  		answer = strings.TrimSpace(answer)
   800  
   801  		if len(answer) == 0 {
   802  			return defaultRecoveryWindow, nil
   803  		}
   804  
   805  		lookAhead, err := strconv.Atoi(answer)
   806  		if err != nil {
   807  			fmt.Printf("Unable to parse recovery window: %v\n", err)
   808  			continue
   809  		}
   810  
   811  		return int32(lookAhead), nil
   812  	}
   813  }
   814  
   815  func askBirthdayTimestamp() (uint64, error) {
   816  	for {
   817  		fmt.Println()
   818  		fmt.Printf("Input an optional wallet birthday unix timestamp " +
   819  			"of first block to start scanning from (default 0): ")
   820  
   821  		reader := bufio.NewReader(os.Stdin)
   822  		answer, err := reader.ReadString('\n')
   823  		if err != nil {
   824  			return 0, err
   825  		}
   826  
   827  		fmt.Println()
   828  
   829  		answer = strings.TrimSpace(answer)
   830  
   831  		if len(answer) == 0 {
   832  			return 0, nil
   833  		}
   834  
   835  		birthdayTimestamp, err := strconv.ParseUint(answer, 10, 64)
   836  		if err != nil {
   837  			fmt.Printf("Unable to parse birthday timestamp: %v\n",
   838  				err)
   839  
   840  			continue
   841  		}
   842  
   843  		return birthdayTimestamp, nil
   844  	}
   845  }
   846  
   847  func printCipherSeedWords(mnemonicWords []string) {
   848  	fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
   849  		"RESTORE THE WALLET!!!")
   850  	fmt.Println()
   851  
   852  	fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
   853  
   854  	numCols := 4
   855  	colWords := monowidthColumns(mnemonicWords, numCols)
   856  	for i := 0; i < len(colWords); i += numCols {
   857  		fmt.Printf("%2d. %3s  %2d. %3s  %2d. %3s  %2d. %3s\n",
   858  			i+1, colWords[i], i+2, colWords[i+1], i+3,
   859  			colWords[i+2], i+4, colWords[i+3])
   860  	}
   861  
   862  	fmt.Println("---------------END LND CIPHER SEED-----------------")
   863  
   864  	fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
   865  		"RESTORE THE WALLET!!!")
   866  }