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 }