You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

502 lines
16 KiB

  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. package main
  17. /// this file implements the wallet and rpc wallet
  18. import "io"
  19. import "os"
  20. import "fmt"
  21. import "time"
  22. import "sync"
  23. import "strings"
  24. import "strconv"
  25. import "runtime"
  26. //import "io/ioutil"
  27. //import "bufio"
  28. //import "bytes"
  29. //import "net/http"
  30. import "encoding/hex"
  31. import "github.com/romana/rlog"
  32. import "github.com/chzyer/readline"
  33. import "github.com/docopt/docopt-go"
  34. import log "github.com/sirupsen/logrus"
  35. import "github.com/vmihailenco/msgpack"
  36. //import "github.com/deroproject/derosuite/address"
  37. import "github.com/deroproject/derosuite/walletapi"
  38. import "github.com/deroproject/derosuite/crypto"
  39. import "github.com/deroproject/derosuite/globals"
  40. import "github.com/deroproject/derosuite/walletapi/mnemonics"
  41. var command_line string = `dero-wallet-cli
  42. DERO : A secure, private blockchain with smart-contracts
  43. Usage:
  44. derod [--help] [--version] [--offline] [--offline_datafile=<file>] [--testnet] [--prompt] [--debug] [--daemon-address=<host:port>] [--restore-deterministic-wallet] [--electrum-seed=<recovery-seed>] [--socks-proxy=<socks_ip:port>]
  45. derod -h | --help
  46. derod --version
  47. Options:
  48. -h --help Show this screen.
  49. --version Show version.
  50. --offline Run the wallet in completely offline mode
  51. --offline_datafile=<file> Use the data in offline mode default ("getoutputs.bin") in current dir
  52. --prompt Disable menu and display prompt
  53. --testnet Run in testnet mode.
  54. --debug Debug mode enabled, print log messages
  55. --restore-deterministic-wallet Restore wallet from previously saved recovery seed
  56. --electrum-seed=<recovery-seed> Seed to use while restoring wallet
  57. --password Password to unlock the wallet
  58. --socks-proxy=<socks_ip:port> Use a proxy to connect to Daemon.
  59. --daemon-address=<host:port> Use daemon instance at <host>:<port>
  60. `
  61. var menu_mode bool = true // default display menu mode
  62. var account_valid bool = false // if an account has been opened, do not allow to create new account in this session
  63. var offline_mode bool // whether we are in offline mode
  64. var sync_in_progress int // whether sync is in progress with daemon
  65. var account *walletapi.Account = &walletapi.Account{} // all account data is available here
  66. var address string
  67. var sync_time time.Time // used to suitable update prompt
  68. var default_offline_datafile string = "getoutputs.bin"
  69. // these pipes are used to feed in transaction data to recover valid amounts
  70. var pipe_reader *io.PipeReader // any output will be read from this end point
  71. var pipe_writer *io.PipeWriter // any data from daemon or file needs to written here
  72. var color_black = "\033[30m"
  73. var color_red = "\033[31m"
  74. var color_green = "\033[32m"
  75. var color_yellow = "\033[33m"
  76. var color_blue = "\033[34m"
  77. var color_magenta = "\033[35m"
  78. var color_cyan = "\033[36m"
  79. var color_white = "\033[37m"
  80. var prompt_mutex sync.Mutex // prompt lock
  81. var prompt string = "\033[92mDERO Wallet:\033[32m>>>\033[0m "
  82. func main() {
  83. var err error
  84. globals.Arguments, err = docopt.Parse(command_line, nil, true, "DERO daemon : work in progress", false)
  85. if err != nil {
  86. log.Fatalf("Error while parsing options err: %s\n", err)
  87. }
  88. // We need to initialize readline first, so it changes stderr to ansi processor on windows
  89. l, err := readline.NewEx(&readline.Config{
  90. //Prompt: "\033[92mDERO:\033[32m»\033[0m",
  91. Prompt: prompt,
  92. HistoryFile: "", // wallet never saves any history file anywhere, to prevent any leakage
  93. AutoComplete: completer,
  94. InterruptPrompt: "^C",
  95. EOFPrompt: "exit",
  96. HistorySearchFold: true,
  97. FuncFilterInputRune: filterInput,
  98. })
  99. if err != nil {
  100. panic(err)
  101. }
  102. defer l.Close()
  103. // parse arguments and setup testnet mainnet
  104. globals.Initialize() // setup network and proxy
  105. globals.Logger.Infof("") // a dummy write is required to fully activate logrus
  106. // all screen output must go through the readline
  107. globals.Logger.Out = l.Stdout()
  108. globals.Logger.Debugf("Arguments %+v", globals.Arguments)
  109. globals.Logger.Infof("DERO Wallet : This version is under heavy development, use it for testing/evaluations purpose only")
  110. globals.Logger.Infof("Copyright 2017-2018 DERO Project. All rights reserved.")
  111. globals.Logger.Infof("OS:%s ARCH:%s GOMAXPROCS:%d", runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
  112. globals.Logger.Infof("Wallet in %s mode", globals.Config.Name)
  113. // disable menu mode if requested
  114. if globals.Arguments["--prompt"].(bool) {
  115. menu_mode = false
  116. }
  117. // lets handle the arguments one by one
  118. if globals.Arguments["--restore-deterministic-wallet"].(bool) {
  119. // user wants to recover wallet, check whether seed is provided on command line, if not prompt now
  120. seed := ""
  121. if globals.Arguments["--electrum-seed"] != nil {
  122. seed = globals.Arguments["--electrum-seed"].(string)
  123. } else { // prompt user for seed
  124. seed = read_line_with_prompt(l, "Enter your seed (25 words) : ")
  125. }
  126. account, err = walletapi.Generate_Account_From_Recovery_Words(seed)
  127. if err != nil {
  128. globals.Logger.Warnf("Error while recovering seed err %s\n", err)
  129. return
  130. }
  131. account_valid = true
  132. globals.Logger.Debugf("Seed Language %s", account.SeedLanguage)
  133. globals.Logger.Infof("Successfully recovered wallet from seed")
  134. address = account.GetAddress().String()
  135. }
  136. // check if offline mode requested
  137. if globals.Arguments["--offline"].(bool) == true {
  138. offline_mode = true
  139. } else { // we are not in offline mode, start communications with the daemon
  140. go Run_Communication_Engine()
  141. }
  142. pipe_reader, pipe_writer = io.Pipe() // create pipes
  143. setPasswordCfg := l.GenPasswordConfig()
  144. setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
  145. l.SetPrompt(fmt.Sprintf("Enter password(%v): ", len(line)))
  146. l.Refresh()
  147. return nil, 0, false
  148. })
  149. l.Refresh() // refresh the prompt
  150. // reader ready to parse any data from the file
  151. go blockchain_data_consumer()
  152. // update prompt when required
  153. go update_prompt(l)
  154. // if wallet has been opened in offline mode by commands supplied at command prompt
  155. // trigger the offline scan
  156. if account_valid {
  157. go trigger_offline_data_scan()
  158. }
  159. // start infinite loop processing user commands
  160. for {
  161. if globals.Exit_In_Progress { // exit if requested so
  162. break
  163. }
  164. if menu_mode { // display menu if requested
  165. if account_valid { // account is opened, display post menu
  166. display_easymenu_post_open_command(l)
  167. } else { // account has not been opened display pre open menu
  168. display_easymenu_pre_open_command(l)
  169. }
  170. }
  171. line, err := l.Readline()
  172. if err == readline.ErrInterrupt {
  173. if len(line) == 0 {
  174. globals.Logger.Infof("Ctrl-C received, Exit in progress\n")
  175. globals.Exit_In_Progress = true
  176. break
  177. } else {
  178. continue
  179. }
  180. } else if err == io.EOF {
  181. break
  182. }
  183. // pass command to suitable handler
  184. if menu_mode {
  185. if account_valid {
  186. handle_easymenu_post_open_command(l, line)
  187. } else {
  188. handle_easymenu_pre_open_command(l, line)
  189. }
  190. } else {
  191. handle_prompt_command(l, line)
  192. }
  193. }
  194. globals.Exit_In_Progress = true
  195. }
  196. // this functions reads all data transferred from daemon or from offline file
  197. // and plays it here
  198. // finds which transactions belong to current account
  199. // and adds them to account for reconciliation
  200. func blockchain_data_consumer() {
  201. var err error
  202. for {
  203. rlog.Tracef(1, "Discarding old pipe_reader,writer, creating new")
  204. // close already created pipes, discarding there data
  205. pipe_reader.Close()
  206. pipe_writer.Close()
  207. pipe_reader, pipe_writer = io.Pipe() // create pipes
  208. decoder := msgpack.NewDecoder(pipe_reader)
  209. for {
  210. var output globals.TX_Output_Data
  211. err = decoder.Decode(&output)
  212. if err == io.EOF { // reached eof
  213. break
  214. }
  215. if err != nil {
  216. fmt.Printf("err while decoding msgpack stream err %s\n", err)
  217. break
  218. }
  219. if globals.Exit_In_Progress {
  220. return
  221. }
  222. sync_time = time.Now()
  223. if account.Index_Global < output.Index_Global { // process tx if it has not been processed earlier
  224. Wallet_Height = output.Height
  225. account.Height = output.Height
  226. if account.Is_Output_Ours(output.Tx_Public_Key, output.Index_within_tx, crypto.Key(output.InKey.Destination)) {
  227. amount, _, result := account.Decode_RingCT_Output(output.Tx_Public_Key,
  228. output.Index_within_tx,
  229. crypto.Key(output.InKey.Mask),
  230. output.ECDHTuple,
  231. output.SigType)
  232. if result == false {
  233. globals.Logger.Warnf("Internal error occurred, amount cannot be spent")
  234. }
  235. globals.Logger.Infof(color_green+"Height %d transaction %s received %s DERO"+color_white, output.Height, output.TXID, globals.FormatMoney(amount))
  236. // add tx to wallet
  237. account.Add_Transaction_Record_Funds(&output)
  238. }
  239. // check this keyimage represents our funds
  240. // if yes we have consumed that specific funds, mark them as such
  241. amount_spent := uint64(0)
  242. for i := range output.Key_Images {
  243. amount_per_keyimage, result := account.Is_Our_Fund_Consumed(output.Key_Images[i])
  244. if result {
  245. amount_spent += amount_per_keyimage
  246. account.Consume_Transaction_Record_Funds(&output, output.Key_Images[i]) // decrease fund from our wallet
  247. }
  248. }
  249. if amount_spent > 0 {
  250. globals.Logger.Infof(color_magenta+"Height %d transaction %s Spent %s DERO"+color_white, output.Height, output.TXID, globals.FormatMoney(amount_spent))
  251. }
  252. account.Index_Global = output.Index_Global
  253. }
  254. }
  255. }
  256. }
  257. // update prompt as and when necessary
  258. // TODO: make this code simple, with clear direction
  259. func update_prompt(l *readline.Instance) {
  260. last_wallet_height := uint64(0)
  261. last_daemon_height := uint64(0)
  262. for {
  263. time.Sleep(30 * time.Millisecond) // give user a smooth running number
  264. if globals.Exit_In_Progress {
  265. return
  266. }
  267. prompt_mutex.Lock() // do not update if we can not lock the mutex
  268. // show first 8 bytes of address
  269. address_trim := ""
  270. if len(address) > 8 {
  271. address_trim = address[0:8]
  272. } else {
  273. address_trim = "DERO Wallet"
  274. }
  275. if len(address) == 0 {
  276. last_wallet_height = 0
  277. Wallet_Height = 0
  278. }
  279. if !account_valid {
  280. l.SetPrompt(fmt.Sprintf("\033[1m\033[32m%s \033[0m"+color_green+"0/%d \033[32m>>>\033[0m ", address_trim, Daemon_Height))
  281. prompt_mutex.Unlock()
  282. continue
  283. }
  284. // only update prompt if needed, trigger resync if required
  285. if last_wallet_height != Wallet_Height || last_daemon_height != Daemon_Height {
  286. // choose color based on urgency
  287. color := "\033[32m" // default is green color
  288. if Wallet_Height < Daemon_Height {
  289. color = "\033[33m" // make prompt yellow
  290. }
  291. balance_string := ""
  292. if account_valid {
  293. balance_unlocked, locked_balance := account.Get_Balance()
  294. balance_string = fmt.Sprintf(color_green+"%s "+color_white+"| "+color_yellow+"%s", globals.FormatMoney(balance_unlocked), globals.FormatMoney(locked_balance))
  295. }
  296. l.SetPrompt(fmt.Sprintf("\033[1m\033[32m%s \033[0m"+color+"%d/%d %s\033[32m>>>\033[0m ", address_trim, Wallet_Height, Daemon_Height, balance_string))
  297. l.Refresh()
  298. last_wallet_height = Wallet_Height
  299. last_daemon_height = Daemon_Height
  300. }
  301. if time.Since(sync_time) > (2*time.Second) && Wallet_Height < Daemon_Height {
  302. if !offline_mode { // if offline mode, never connect anywhere
  303. go Get_Outputs(account.Index_Global, 0) // start sync
  304. }
  305. sync_time = time.Now()
  306. }
  307. prompt_mutex.Unlock()
  308. }
  309. }
  310. // create a new wallet from scratch from random numbers
  311. func Create_New_Account(l *readline.Instance) *walletapi.Account {
  312. account, _ := walletapi.Generate_Keys_From_Random()
  313. account.SeedLanguage = choose_seed_language(l)
  314. // a new account has been created, append the seed to user home directory
  315. //usr, err := user.Current()
  316. /*if err != nil {
  317. globals.Logger.Warnf("Cannot get current username to save recovery key and password")
  318. }else{ // we have a user, get his home dir
  319. }*/
  320. return account
  321. }
  322. // create a new wallet from hex seed provided
  323. func Create_New_Account_from_seed(l *readline.Instance) *walletapi.Account {
  324. var account *walletapi.Account
  325. var seedkey crypto.Key
  326. seed := read_line_with_prompt(l, "Please enter your seed ( hex 64 chars): ")
  327. seed = strings.TrimSpace(seed) // trim any extra space
  328. seed_raw, err := hex.DecodeString(seed) // hex decode
  329. if len(seed) != 64 || err != nil { //sanity check
  330. globals.Logger.Warnf("Seed must be 64 chars hexadecimal chars")
  331. return account
  332. }
  333. copy(seedkey[:], seed_raw[:32]) // copy bytes to seed
  334. account, _ = walletapi.Generate_Account_From_Seed(seedkey) // create a new account
  335. account.SeedLanguage = choose_seed_language(l) // ask user his seed preference and set it
  336. account_valid = true
  337. return account
  338. }
  339. // create a new wallet from viewable seed provided
  340. // viewable seed consists of public spend key and private view key
  341. func Create_New_Account_from_viewable_key(l *readline.Instance) *walletapi.Account {
  342. var seedkey crypto.Key
  343. var privateview crypto.Key
  344. var account *walletapi.Account
  345. seed := read_line_with_prompt(l, "Please enter your View Only Key ( hex 128 chars): ")
  346. seed = strings.TrimSpace(seed) // trim any extra space
  347. seed_raw, err := hex.DecodeString(seed)
  348. if len(seed) != 128 || err != nil {
  349. globals.Logger.Warnf("View Only key must be 128 chars hexadecimal chars")
  350. return account
  351. }
  352. copy(seedkey[:], seed_raw[:32])
  353. copy(privateview[:], seed_raw[32:64])
  354. account, _ = walletapi.Generate_Account_View_Only(seedkey, privateview)
  355. account_valid = true
  356. return account
  357. }
  358. // helper function to let user to choose a seed in specific lanaguage
  359. func choose_seed_language(l *readline.Instance) string {
  360. languages := mnemonics.Language_List()
  361. fmt.Printf("Language list for seeds, please enter a number (default English)\n")
  362. for i := range languages {
  363. fmt.Fprintf(l.Stderr(), "\033[1m%2d:\033[0m %s\n", i, languages[i])
  364. }
  365. language_number := read_line_with_prompt(l, "Please enter a choice: ")
  366. choice := 0 // 0 for english
  367. if s, err := strconv.Atoi(language_number); err == nil {
  368. choice = s
  369. }
  370. for i := range languages { // if user gave any wrong or ot of range choice, choose english
  371. if choice == i {
  372. return languages[choice]
  373. }
  374. }
  375. // if no match , return Englisg
  376. return "English"
  377. }
  378. // read a line from the prompt
  379. func read_line_with_prompt(l *readline.Instance, prompt_temporary string) string {
  380. prompt_mutex.Lock()
  381. defer prompt_mutex.Unlock()
  382. l.SetPrompt(prompt_temporary)
  383. line, err := l.Readline()
  384. if err == readline.ErrInterrupt {
  385. if len(line) == 0 {
  386. globals.Logger.Infof("Ctrl-C received, Exiting\n")
  387. os.Exit(0)
  388. }
  389. } else if err == io.EOF {
  390. os.Exit(0)
  391. }
  392. l.SetPrompt(prompt)
  393. return line
  394. }
  395. // filter out specfic inputs from input processing
  396. // currently we only skip CtrlZ background key
  397. func filterInput(r rune) (rune, bool) {
  398. switch r {
  399. // block CtrlZ feature
  400. case readline.CharCtrlZ:
  401. return r, false
  402. }
  403. return r, true
  404. }