github.com/deroproject/derosuite@v2.1.6-1.0.20200307070847-0f2e589c7a2b+incompatible/cmd/dero-wallet-cli/main.go (about)

     1  // Copyright 2017-2018 DERO Project. All rights reserved.
     2  // Use of this source code in any form is governed by RESEARCH license.
     3  // license can be found in the LICENSE file.
     4  // GPG: 0F39 E425 8C65 3947 702A  8234 08B2 0360 A03A 9DE8
     5  //
     6  //
     7  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
     8  // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
     9  // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
    10  // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    11  // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    12  // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    13  // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
    14  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
    15  // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    16  
    17  package main
    18  
    19  /// this file implements the wallet and rpc wallet
    20  
    21  import "io"
    22  import "os"
    23  import "fmt"
    24  import "time"
    25  import "sync"
    26  import "strings"
    27  import "strconv"
    28  import "runtime"
    29  
    30  import "sync/atomic"
    31  
    32  //import "io/ioutil"
    33  //import "bufio"
    34  //import "bytes"
    35  //import "net/http"
    36  //import "encoding/hex"
    37  
    38  import "github.com/romana/rlog"
    39  import "github.com/chzyer/readline"
    40  import "github.com/docopt/docopt-go"
    41  import log "github.com/sirupsen/logrus"
    42  
    43  //import "github.com/vmihailenco/msgpack"
    44  
    45  //import "github.com/deroproject/derosuite/address"
    46  
    47  import "github.com/deroproject/derosuite/config"
    48  import "github.com/deroproject/derosuite/globals"
    49  import "github.com/deroproject/derosuite/walletapi"
    50  import "github.com/deroproject/derosuite/walletapi/mnemonics"
    51  
    52  var command_line string = `dero-wallet-cli 
    53  DERO : A secure, private blockchain with smart-contracts
    54  
    55  Usage:
    56    dero-wallet-cli [options] 
    57    dero-wallet-cli -h | --help
    58    dero-wallet-cli --version
    59  
    60    Options:
    61    -h --help     Show this screen.
    62    --version     Show version.
    63    --wallet-file=<file>  Use this file to restore or create new wallet
    64    --password=<password>  Use this password to unlock the wallet
    65    --offline     Run the wallet in completely offline mode 
    66    --offline_datafile=<file>  Use the data in offline mode default ("getoutputs.bin") in current dir
    67    --prompt      Disable menu and display prompt
    68    --testnet  	Run in testnet mode.
    69    --debug       Debug mode enabled, print log messages
    70    --unlocked    Keep wallet unlocked for cli commands (Does not confirm password before commands)
    71    --generate-new-wallet Generate new wallet
    72    --restore-deterministic-wallet    Restore wallet from previously saved recovery seed
    73    --electrum-seed=<recovery-seed>   Seed to use while restoring wallet
    74    --socks-proxy=<socks_ip:port>  Use a proxy to connect to Daemon.
    75    --remote      use hard coded remote daemon https://rwallet.dero.live
    76    --daemon-address=<host:port>    Use daemon instance at <host>:<port> or https://domain
    77    --rpc-server      Run rpc server, so wallet is accessible using api
    78    --rpc-bind=<127.0.0.1:20209>  Wallet binds on this ip address and port
    79    --rpc-login=<username:password>  RPC server will grant access based on these credentials
    80    `
    81  var menu_mode bool = true // default display menu mode
    82  //var account_valid bool = false                        // if an account has been opened, do not allow to create new account in this session
    83  var offline_mode bool        // whether we are in offline mode
    84  var sync_in_progress int     //  whether sync is in progress with daemon
    85  var wallet *walletapi.Wallet //= &walletapi.Account{} // all account  data is available here
    86  //var address string
    87  var sync_time time.Time // used to suitable update  prompt
    88  
    89  var default_offline_datafile string = "getoutputs.bin"
    90  
    91  var color_black = "\033[30m"
    92  var color_red = "\033[31m"
    93  var color_green = "\033[32m"
    94  var color_yellow = "\033[33m"
    95  var color_blue = "\033[34m"
    96  var color_magenta = "\033[35m"
    97  var color_cyan = "\033[36m"
    98  var color_white = "\033[37m"
    99  var color_extra_white = "\033[1m"
   100  var color_normal = "\033[0m"
   101  
   102  var prompt_mutex sync.Mutex // prompt lock
   103  var prompt string = "\033[92mDERO Wallet:\033[32m>>>\033[0m "
   104  
   105  var tablock uint32
   106  
   107  func main() {
   108  
   109  	var err error
   110  
   111  	globals.Init_rlog()
   112  
   113  	globals.Arguments, err = docopt.Parse(command_line, nil, true, "DERO atlantis wallet : work in progress", false)
   114  	//globals.Arguments, err = docopt.ParseArgs(command_line, os.Args[1:],  "DERO daemon : work in progress")
   115  	if err != nil {
   116  		log.Fatalf("Error while parsing options err: %s\n", err)
   117  	}
   118  
   119  	// We need to initialize readline first, so it changes stderr to ansi processor on windows
   120  	l, err := readline.NewEx(&readline.Config{
   121  		//Prompt:          "\033[92mDERO:\033[32m»\033[0m",
   122  		Prompt:          prompt,
   123  		HistoryFile:     "", // wallet never saves any history file anywhere, to prevent any leakage
   124  		AutoComplete:    completer,
   125  		InterruptPrompt: "^C",
   126  		EOFPrompt:       "exit",
   127  
   128  		HistorySearchFold:   true,
   129  		FuncFilterInputRune: filterInput,
   130  	})
   131  	if err != nil {
   132  		panic(err)
   133  	}
   134  	defer l.Close()
   135  
   136  	// get ready to grab passwords
   137  	setPasswordCfg := l.GenPasswordConfig()
   138  	setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
   139  		l.SetPrompt(fmt.Sprintf("Enter password(%v): ", len(line)))
   140  		l.Refresh()
   141  		return nil, 0, false
   142  	})
   143  	l.Refresh() // refresh the prompt
   144  
   145  	// parse arguments and setup testnet mainnet
   146  	globals.Initialize()     // setup network and proxy
   147  	globals.Logger.Infof("") // a dummy write is required to fully activate logrus
   148  
   149  	// all screen output must go through the readline
   150  	globals.Logger.Out = l.Stdout()
   151  
   152  	rlog.Infof("Arguments %+v", globals.Arguments)
   153  	globals.Logger.Infof("DERO Wallet : %s  This version is under heavy development, use it for testing/evaluations purpose only", config.Version.String())
   154  	globals.Logger.Infof("Copyright 2017-2018 DERO Project. All rights reserved.")
   155  	globals.Logger.Infof("OS:%s ARCH:%s GOMAXPROCS:%d", runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
   156  	globals.Logger.Infof("Wallet in %s mode", globals.Config.Name)
   157  
   158  	// disable menu mode if requested
   159  	if globals.Arguments["--prompt"] != nil && globals.Arguments["--prompt"].(bool) {
   160  		menu_mode = false
   161  	}
   162  
   163  	wallet_file := "wallet.db" //dero.wallet"
   164  	if globals.Arguments["--wallet-file"] != nil {
   165  		wallet_file = globals.Arguments["--wallet-file"].(string) // override with user specified settings
   166  	}
   167  
   168  	wallet_password := "" // default
   169  	if globals.Arguments["--password"] != nil {
   170  		wallet_password = globals.Arguments["--password"].(string) // override with user specified settings
   171  	}
   172  
   173  	// lets handle the arguments one by one
   174  	if globals.Arguments["--restore-deterministic-wallet"].(bool) {
   175  		// user wants to recover wallet, check whether seed is provided on command line, if not prompt now
   176  		seed := ""
   177  
   178  		if globals.Arguments["--electrum-seed"] != nil {
   179  			seed = globals.Arguments["--electrum-seed"].(string)
   180  		} else { // prompt user for seed
   181  			seed = read_line_with_prompt(l, "Enter your seed (25 words) : ")
   182  		}
   183  
   184  		account, err := walletapi.Generate_Account_From_Recovery_Words(seed)
   185  		if err != nil {
   186  			globals.Logger.Warnf("Error while recovering seed err %s\n", err)
   187  			return
   188  		}
   189  
   190  		// ask user a pass, if not provided on command_line
   191  		password := ""
   192  		if wallet_password == "" {
   193  			password = ReadConfirmedPassword(l, "Enter password", "Confirm password")
   194  		}
   195  
   196  		wallet, err = walletapi.Create_Encrypted_Wallet(wallet_file, password, account.Keys.Spendkey_Secret)
   197  		if err != nil {
   198  			globals.Logger.Warnf("Error occurred while restoring wallet. err %s", err)
   199  			return
   200  		}
   201  
   202  		globals.Logger.Debugf("Seed Language %s", account.SeedLanguage)
   203  		globals.Logger.Infof("Successfully recovered wallet from seed")
   204  
   205  	}
   206  
   207  	// generare new random account if requested
   208  	if globals.Arguments["--generate-new-wallet"] != nil && globals.Arguments["--generate-new-wallet"].(bool) {
   209  		filename := choose_file_name(l)
   210  		// ask user a pass, if not provided on command_line
   211  		password := ""
   212  		if wallet_password == "" {
   213  			password = ReadConfirmedPassword(l, "Enter password", "Confirm password")
   214  		}
   215  
   216  		seed_language := choose_seed_language(l)
   217  		wallet, err = walletapi.Create_Encrypted_Wallet_Random(filename, password)
   218  		if err != nil {
   219  			globals.Logger.Warnf("Error occured while creating new wallet, err: %s", err)
   220  			wallet = nil
   221  			return
   222  
   223  		}
   224  		globals.Logger.Debugf("Seed Language %s", seed_language)
   225  		display_seed(l, wallet)
   226  
   227  	}
   228  
   229  	if globals.Arguments["--rpc-login"] != nil {
   230  		userpass := globals.Arguments["--rpc-login"].(string)
   231  		parts := strings.SplitN(userpass, ":", 2)
   232  
   233  		if len(parts) != 2 {
   234  			globals.Logger.Warnf("RPC user name or password invalid")
   235  			return
   236  		}
   237  		log.Infof("RPC username \"%s\" password \"%s\" ", parts[0], parts[1])
   238  	}
   239  
   240  	// if wallet is nil,  check whether the file exists, if yes, request password
   241  	if wallet == nil {
   242  		if _, err = os.Stat(wallet_file); err == nil {
   243  
   244  			// if a wallet file and password  has been provide, make sure that the wallet opens in 1st attempt, othwer wise exit
   245  
   246  			if globals.Arguments["--password"] != nil {
   247  				wallet, err = walletapi.Open_Encrypted_Wallet(wallet_file, wallet_password)
   248  				if err != nil {
   249  					globals.Logger.Warnf("Error occurred while opening wallet. err %s", err)
   250  					os.Exit(-1)
   251  				}
   252  			} else { // request user the password
   253  
   254  				// ask user a password
   255  				for i := 0; i < 3; i++ {
   256  					wallet, err = walletapi.Open_Encrypted_Wallet(wallet_file, ReadPassword(l, wallet_file))
   257  					if err != nil {
   258  						globals.Logger.Warnf("Error occurred while opening wallet. err %s", err)
   259  					} else { //  user knows the password and is db is valid
   260  						break
   261  					}
   262  				}
   263  			}
   264  
   265  			//globals.Logger.Debugf("Seed Language %s", account.SeedLanguage)
   266  			//globals.Logger.Infof("Successfully recovered wallet from seed")
   267  
   268  		}
   269  	}
   270  
   271  	// check if offline mode requested
   272  	if wallet != nil {
   273  		common_processing(wallet)
   274  	}
   275  
   276  	//pipe_reader, pipe_writer = io.Pipe() // create pipes
   277  
   278  	// reader ready to parse any data from the file
   279  	//go blockchain_data_consumer()
   280  
   281  	// update prompt when required
   282  	prompt_mutex.Lock()
   283  	go update_prompt(l)
   284  	prompt_mutex.Unlock()
   285  
   286  	// if wallet has been opened in offline mode by commands supplied at command prompt
   287  	// trigger the offline scan
   288  
   289  	//	go trigger_offline_data_scan()
   290  
   291  	// start infinite loop processing user commands
   292  	for {
   293  
   294  		prompt_mutex.Lock()
   295  		if globals.Exit_In_Progress { // exit if requested so
   296  			prompt_mutex.Unlock()
   297  			break
   298  		}
   299  		prompt_mutex.Unlock()
   300  
   301  		if menu_mode { // display menu if requested
   302  			if wallet != nil { // account is opened, display post menu
   303  				display_easymenu_post_open_command(l)
   304  			} else { // account has not been opened display pre open menu
   305  				display_easymenu_pre_open_command(l)
   306  			}
   307  		}
   308  
   309  		line, err := l.Readline()
   310  		if err == readline.ErrInterrupt {
   311  			if len(line) == 0 {
   312  				globals.Logger.Infof("Ctrl-C received, Exit in progress\n")
   313  				globals.Exit_In_Progress = true
   314  				break
   315  			} else {
   316  				continue
   317  			}
   318  		} else if err == io.EOF {
   319  //			break
   320  			time.Sleep(time.Second)
   321  		}
   322  
   323  		// pass command to suitable handler
   324  		if menu_mode {
   325  			if wallet != nil {
   326  				if !handle_easymenu_post_open_command(l, line) { // if not processed , try processing as command
   327  					handle_prompt_command(l, line)
   328  					PressAnyKey(l, wallet)
   329  				}
   330  			} else {
   331  				handle_easymenu_pre_open_command(l, line)
   332  			}
   333  		} else {
   334  			handle_prompt_command(l, line)
   335  		}
   336  
   337  	}
   338  	prompt_mutex.Lock()
   339  	globals.Exit_In_Progress = true
   340  	prompt_mutex.Unlock()
   341  
   342  }
   343  
   344  // update prompt as and when necessary
   345  // TODO: make this code simple, with clear direction
   346  func update_prompt(l *readline.Instance) {
   347  
   348  	last_wallet_height := uint64(0)
   349  	last_daemon_height := uint64(0)
   350  	daemon_online := false
   351  	last_update_time := int64(0)
   352  
   353  	for {
   354  		time.Sleep(30 * time.Millisecond) // give user a smooth running number
   355  
   356  		prompt_mutex.Lock()
   357  		if globals.Exit_In_Progress {
   358  			prompt_mutex.Unlock()
   359  			return
   360  		}
   361  		prompt_mutex.Unlock()
   362  
   363  		if atomic.LoadUint32(&tablock) > 0 { // tab key has been presssed,  stop delivering updates to  prompt
   364  			continue
   365  		}
   366  
   367  		prompt_mutex.Lock() // do not update if we can not lock the mutex
   368  
   369  		// show first 8 bytes of address
   370  		address_trim := ""
   371  		if wallet != nil {
   372  			tmp_addr := wallet.GetAddress().String()
   373  			address_trim = tmp_addr[0:8]
   374  		} else {
   375  			address_trim = "DERO Wallet"
   376  		}
   377  
   378  		if wallet == nil {
   379  			l.SetPrompt(fmt.Sprintf("\033[1m\033[32m%s \033[0m"+color_green+"0/%d \033[32m>>>\033[0m ", address_trim, 0))
   380  			prompt_mutex.Unlock()
   381  			continue
   382  		}
   383  
   384  		// only update prompt if needed, or update atleast once every second
   385  
   386  		if last_wallet_height != wallet.Get_Height() || last_daemon_height != wallet.Get_Daemon_Height() ||
   387  			daemon_online != wallet.IsDaemonOnlineCached() || (time.Now().Unix()-last_update_time) >= 1 {
   388  			// choose color based on urgency
   389  			color := "\033[32m" // default is green color
   390  			if wallet.Get_Height() < wallet.Get_Daemon_Height() {
   391  				color = "\033[33m" // make prompt yellow
   392  			}
   393  
   394  			dheight := wallet.Get_Daemon_Height()
   395  
   396  			if wallet.IsDaemonOnlineCached() == false {
   397  				color = "\033[33m" // make prompt yellow
   398  				dheight = 0
   399  			}
   400  
   401  			balance_string := ""
   402  
   403  			//balance_unlocked, locked_balance := wallet.Get_Balance_Rescan()// wallet.Get_Balance()
   404  			balance_unlocked, locked_balance := wallet.Get_Balance()
   405  			balance_string = fmt.Sprintf(color_green+"%s "+color_white+"| "+color_yellow+"%s", globals.FormatMoney8(balance_unlocked), globals.FormatMoney8(locked_balance))
   406  
   407  			testnet_string := ""
   408  			if !globals.IsMainnet() {
   409  				testnet_string = "\033[31m TESTNET"
   410  			}
   411  
   412  			l.SetPrompt(fmt.Sprintf("\033[1m\033[32m%s \033[0m"+color+"%d/%d %s %s\033[32m>>>\033[0m ", address_trim, wallet.Get_Height(), dheight, balance_string, testnet_string))
   413  			l.Refresh()
   414  			last_wallet_height = wallet.Get_Height()
   415  			last_daemon_height = wallet.Get_Daemon_Height()
   416  			last_update_time = time.Now().Unix()
   417  			daemon_online = wallet.IsDaemonOnlineCached()
   418  			_ = last_update_time
   419  
   420  		}
   421  
   422  		prompt_mutex.Unlock()
   423  
   424  	}
   425  
   426  }
   427  
   428  // create a new wallet from scratch from random numbers
   429  func Create_New_Wallet(l *readline.Instance) (w *walletapi.Wallet, err error) {
   430  
   431  	// ask user a file name to store the data
   432  
   433  	walletpath := read_line_with_prompt(l, "Please enter wallet file name : ")
   434  	walletpassword := ""
   435  
   436  	account, _ := walletapi.Generate_Keys_From_Random()
   437  	account.SeedLanguage = choose_seed_language(l)
   438  
   439  	w, err = walletapi.Create_Encrypted_Wallet(walletpath, walletpassword, account.Keys.Spendkey_Secret)
   440  
   441  	if err != nil {
   442  		return
   443  	}
   444  
   445  	// set wallet seed language
   446  
   447  	// a new account has been created, append the seed to user home directory
   448  
   449  	//usr, err := user.Current()
   450  	/*if err != nil {
   451  	      globals.Logger.Warnf("Cannot get current username to save recovery key and password")
   452  	  }else{ // we have a user, get his home dir
   453  
   454  
   455  	  }*/
   456  
   457  	return
   458  }
   459  
   460  /*
   461  
   462  // create a new wallet from hex seed provided
   463  func Create_New_Account_from_seed(l *readline.Instance) *walletapi.Account {
   464  
   465  	var account *walletapi.Account
   466  	var seedkey crypto.Key
   467  
   468  	seed := read_line_with_prompt(l, "Please enter your seed ( hex 64 chars): ")
   469  	seed = strings.TrimSpace(seed)          // trim any extra space
   470  	seed_raw, err := hex.DecodeString(seed) // hex decode
   471  	if len(seed) != 64 || err != nil {      //sanity check
   472  		globals.Logger.Warnf("Seed must be 64 chars hexadecimal chars")
   473  		return account
   474  	}
   475  
   476  	copy(seedkey[:], seed_raw[:32])                            // copy bytes to seed
   477  	account, _ = walletapi.Generate_Account_From_Seed(seedkey) // create a new account
   478  	account.SeedLanguage = choose_seed_language(l)             // ask user his seed preference and set it
   479  
   480  	account_valid = true
   481  
   482  	return account
   483  }
   484  
   485  // create a new wallet from viewable seed provided
   486  // viewable seed consists of public spend key and private view key
   487  func Create_New_Account_from_viewable_key(l *readline.Instance) *walletapi.Account {
   488  
   489  	var seedkey crypto.Key
   490  	var privateview crypto.Key
   491  
   492  	var account *walletapi.Account
   493  	seed := read_line_with_prompt(l, "Please enter your View Only Key ( hex 128 chars): ")
   494  
   495  	seed = strings.TrimSpace(seed) // trim any extra space
   496  
   497  	seed_raw, err := hex.DecodeString(seed)
   498  	if len(seed) != 128 || err != nil {
   499  		globals.Logger.Warnf("View Only key must be 128 chars hexadecimal chars")
   500  		return account
   501  	}
   502  
   503  	copy(seedkey[:], seed_raw[:32])
   504  	copy(privateview[:], seed_raw[32:64])
   505  
   506  	account, _ = walletapi.Generate_Account_View_Only(seedkey, privateview)
   507  
   508  	account_valid = true
   509  
   510  	return account
   511  }
   512  */
   513  // helper function to let user to choose a seed in specific lanaguage
   514  func choose_seed_language(l *readline.Instance) string {
   515  	languages := mnemonics.Language_List()
   516  	fmt.Printf("Language list for seeds, please enter a number (default English)\n")
   517  	for i := range languages {
   518  		fmt.Fprintf(l.Stderr(), "\033[1m%2d:\033[0m %s\n", i, languages[i])
   519  	}
   520  
   521  	language_number := read_line_with_prompt(l, "Please enter a choice: ")
   522  	choice := 0 // 0 for english
   523  
   524  	if s, err := strconv.Atoi(language_number); err == nil {
   525  		choice = s
   526  	}
   527  
   528  	for i := range languages { // if user gave any wrong or ot of range choice, choose english
   529  		if choice == i {
   530  			return languages[choice]
   531  		}
   532  	}
   533  	// if no match , return Englisg
   534  	return "English"
   535  
   536  }
   537  
   538  // lets the user choose a filename or use default
   539  func choose_file_name(l *readline.Instance) (filename string) {
   540  
   541  	default_filename := "wallet.db"
   542  	if globals.Arguments["--wallet-file"] != nil {
   543  		default_filename = globals.Arguments["--wallet-file"].(string) // override with user specified settings
   544  	}
   545  
   546  	filename = read_line_with_prompt(l, fmt.Sprintf("Enter wallet filename (default %s): ", default_filename))
   547  
   548  	if len(filename) < 1 {
   549  		filename = default_filename
   550  	}
   551  
   552  	return
   553  }
   554  
   555  // read a line from the prompt
   556  // since we cannot query existing, we can get away by using password mode with
   557  func read_line_with_prompt(l *readline.Instance, prompt_temporary string) string {
   558  	prompt_mutex.Lock()
   559  	defer prompt_mutex.Unlock()
   560  	l.SetPrompt(prompt_temporary)
   561  	line, err := l.Readline()
   562  	if err == readline.ErrInterrupt {
   563  		if len(line) == 0 {
   564  			globals.Logger.Infof("Ctrl-C received, Exiting\n")
   565  			os.Exit(0)
   566  		}
   567  	} else if err == io.EOF {
   568  		os.Exit(0)
   569  	}
   570  	l.SetPrompt(prompt)
   571  	return line
   572  
   573  }
   574  
   575  // filter out specfic inputs from input processing
   576  // currently we only skip CtrlZ background key
   577  func filterInput(r rune) (rune, bool) {
   578  	switch r {
   579  	// block CtrlZ feature
   580  	case readline.CharCtrlZ:
   581  		return r, false
   582  	case readline.CharTab:
   583  		atomic.StoreUint32(&tablock, 1) // lock prompt update
   584  	case readline.CharEnter:
   585  		atomic.StoreUint32(&tablock, 0) // enable prompt update
   586  	}
   587  	return r, true
   588  }