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 }