Comment on wallet classes. Use address_is_old to check if address is beyond gap limit
[electrum-nvc.git] / electrum
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 from decimal import Decimal
20 import json
21 import optparse
22 import os
23 import re
24 import ast
25 import sys
26 import time
27 import traceback
28
29
30 is_local = os.path.dirname(os.path.realpath(__file__)) == os.getcwd()
31 is_android = 'ANDROID_DATA' in os.environ
32
33 import __builtin__
34 __builtin__.use_local_modules = is_local or is_android
35
36 # load local module as electrum
37 if __builtin__.use_local_modules:
38     import imp
39     imp.load_module('electrum', *imp.find_module('lib'))
40     imp.load_module('electrum_gui', *imp.find_module('gui'))
41
42 if is_local:
43     sys.path.append('packages')
44
45
46 from electrum import SimpleConfig, Network, Wallet, WalletStorage, NetworkProxy, Commands, known_commands, pick_random_server
47 from electrum.util import print_msg, print_stderr, print_json, set_verbosity
48
49 # get password routine
50 def prompt_password(prompt, confirm=True):
51     import getpass
52     if sys.stdin.isatty():
53         password = getpass.getpass(prompt)
54         if password and confirm:
55             password2 = getpass.getpass("Confirm: ")
56             if password != password2:
57                 sys.exit("Error: Passwords do not match.")
58     else:
59         password = raw_input(prompt)
60     if not password:
61         password = None
62     return password
63
64
65 def arg_parser():
66     usage = "%prog [options] command"
67     parser = optparse.OptionParser(prog=usage, add_help_option=False)
68     parser.add_option("-h", "--help", action="callback", callback=print_help_cb, help="show this help text")
69     parser.add_option("-g", "--gui", dest="gui", help="User interface: qt, lite, gtk, text or stdio")
70     parser.add_option("-w", "--wallet", dest="wallet_path", help="wallet path (default: electrum.dat)")
71     parser.add_option("-o", "--offline", action="store_true", dest="offline", default=False, help="remain offline")
72     parser.add_option("-C", "--concealed", action="store_true", dest="concealed", default=False, help="don't echo seed to console when restoring")
73     parser.add_option("-a", "--all", action="store_true", dest="show_all", default=False, help="show all addresses")
74     parser.add_option("-l", "--labels", action="store_true", dest="show_labels", default=False, help="show the labels of listed addresses")
75     parser.add_option("-f", "--fee", dest="tx_fee", default=None, help="set tx fee")
76     parser.add_option("-F", "--fromaddr", dest="from_addr", default=None, help="set source address for payto/mktx. if it isn't in the wallet, it will ask for the private key unless supplied in the format public_key:private_key. It's not saved in the wallet.")
77     parser.add_option("-c", "--changeaddr", dest="change_addr", default=None, help="set the change address for payto/mktx. default is a spare address, or the source address if it's not in the wallet")
78     parser.add_option("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp), h (http), s (tcp+ssl), or g (https)")
79     parser.add_option("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
80     parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="show debugging information")
81     parser.add_option("-P", "--portable", action="store_true", dest="portable", default=False, help="portable wallet")
82     parser.add_option("-L", "--lang", dest="language", default=None, help="defaut language used in GUI")
83     parser.add_option("-u", "--usb", dest="bitkey", action="store_true", help="Turn on support for hardware wallets (EXPERIMENTAL)")
84     parser.add_option("-G", "--gap", dest="gap_limit", default=None, help="gap limit")
85     parser.add_option("-W", "--password", dest="password", default=None, help="set password for usage with commands (currently only implemented for create command, do not use it for longrunning gui session since the password is visible in /proc)")
86     parser.add_option("-1", "--oneserver", action="store_true", dest="oneserver", default=False, help="connect to one server only")
87     parser.add_option("--bip32", action="store_true", dest="bip32", default=False, help="bip32 (not final)")
88     parser.add_option("--2of3", action="store_true", dest="2of3", default=False, help="create 2of3 wallet")
89     parser.add_option("--mpk", dest="mpk", default=False, help="restore from master public key")
90     parser.add_option("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
91     return parser
92
93
94 def print_help(parser):
95     parser.print_help()
96     print_msg("Type 'electrum help <command>' to see the help for a specific command")
97     print_msg("Type 'electrum --help' to see the list of options")
98     run_command(known_commands['help'])
99     sys.exit(1)
100
101
102 def print_help_cb(self, opt, value, parser):
103     print_help(parser)
104
105
106 def run_command(cmd, password=None, args=[]):
107     import socket
108     if cmd.requires_network and not options.offline:
109         network = NetworkProxy(config)
110         if not network.start(start_daemon= (True if cmd.name!='daemon' else False)):
111             print "Daemon not running"
112             sys.exit(1)
113
114
115
116         if wallet:
117             wallet.start_threads(network)
118             wallet.update()
119     else:
120         network = None
121
122     cmd_runner = Commands(wallet, network)
123     func = getattr(cmd_runner, cmd.name)
124     cmd_runner.password = password
125     try:
126         result = func(*args[1:])
127     except Exception:
128         traceback.print_exc(file=sys.stdout)
129         sys.exit(1)
130
131
132     if cmd.requires_network and not options.offline:
133         if wallet:
134             wallet.stop_threads()
135
136
137     if type(result) == str:
138         print_msg(result)
139     elif result is not None:
140         print_json(result)
141
142
143
144
145
146
147 if __name__ == '__main__':
148
149     parser = arg_parser()
150     options, args = parser.parse_args()
151     if options.portable and options.wallet_path is None:
152         options.electrum_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data')
153
154     # config is an object passed to the various constructors (wallet, interface, gui)
155     if is_android:
156         config_options = {
157             'portable': True,
158             'verbose': True,
159             'gui': 'android',
160             'auto_cycle': True,
161         }
162     else:
163         config_options = eval(str(options))
164         for k, v in config_options.items():
165             if v is None:
166                 config_options.pop(k)
167
168     set_verbosity(config_options.get('verbose'))
169
170     config = SimpleConfig(config_options)
171
172     if len(args) == 0:
173         url = None
174         cmd = 'gui'
175     elif len(args) == 1 and re.match('^bitcoin:', args[0]):
176         url = args[0]
177         cmd = 'gui'
178     else:
179         cmd = args[0]
180
181     if cmd == 'gui':
182         gui_name = config.get('gui', 'classic')
183         if gui_name in ['lite', 'classic']:
184             gui_name = 'qt'
185         try:
186             gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui'])
187         except ImportError:
188             traceback.print_exc(file=sys.stdout)
189             sys.exit()
190             #sys.exit("Error: Unknown GUI: " + gui_name )
191
192         # network interface
193         if not options.offline:
194             network = Network(config)
195             network.start()
196         else:
197             network = None
198
199         gui = gui.ElectrumGui(config, network)
200         gui.main(url)
201
202         if network:
203             network.stop()
204
205         # we use daemon threads, their termination is enforced.
206         # this sleep command gives them time to terminate cleanly.
207         time.sleep(0.1)
208         sys.exit(0)
209
210     if cmd not in known_commands:
211         cmd = 'help'
212
213     cmd = known_commands[cmd]
214
215     # instanciate wallet for command-line
216     storage = WalletStorage(config)
217
218
219     if cmd.name in ['create', 'restore']:
220         if storage.file_exists:
221             sys.exit("Error: Remove the existing wallet first!")
222         if options.password is not None:
223             password = options.password
224         elif cmd.name == 'restore' and options.mpk:
225             password = None
226         else:
227             password = prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
228
229         # if config.server is set, the user either passed the server on command line
230         # or chose it previously already. if he didn't pass a server on the command line,
231         # we just pick up a random one.
232         if not config.get('server'):
233             config.set_key('server', pick_random_server())
234
235         #fee = options.tx_fee if options.tx_fee else raw_input("fee (default:%s):" % (str(Decimal(wallet.fee)/100000000)))
236         #gap = options.gap_limit if options.gap_limit else raw_input("gap limit (default 5):")
237         #if fee:
238         #    wallet.set_fee(float(fee)*100000000)
239         #if gap:
240         #    wallet.change_gap_limit(int(gap))
241
242         if cmd.name == 'restore':
243             if options.mpk:
244                 wallet = Wallet.from_mpk(options.mpk, storage)
245             else:
246                 import getpass
247                 seed = getpass.getpass(prompt="seed:", stream=None) if options.concealed else raw_input("seed:")
248                 if not Wallet.is_seed(seed):
249                     sys.exit("Error: Invalid seed")
250                 wallet = Wallet.from_seed(seed, storage)
251                 wallet.add_seed(seed, password)
252                 wallet.create_accounts(password)
253
254             if not options.offline:
255                 network = Network(config)
256                 network.start()
257                 wallet.start_threads(network)
258                 print_msg("Recovering wallet...")
259                 wallet.restore(lambda x: x)
260                 if wallet.is_found():
261                     print_msg("Recovery successful")
262                 else:
263                     print_msg("Warning: Found no history for this wallet")
264             else:
265                 wallet.synchronize()
266                 print_msg("Warning: This wallet was restored offline. It may contain more addresses than displayed.")
267
268         else:
269             if not config.get('2of3'):
270                 wallet = Wallet(storage)
271                 seed = wallet.make_seed()
272                 wallet.add_seed(seed, password)
273                 wallet.create_accounts(password)
274                 wallet.synchronize()
275                 print_msg("Your wallet generation seed is:\n\"%s\"" % seed)
276                 print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
277             else:
278                 wallet = Wallet_2of3(storage)
279                 cold_seed = wallet.make_seed()
280                 #wallet.save_seed()
281                 print_msg("Your cold seed is:\n\"%s\"" % cold_seed)
282                 print_msg("Please store it on paper. ")
283                 print_msg("Open this file on your online computer to complete your wallet creation.")
284
285
286         print_msg("Wallet saved in '%s'" % wallet.storage.path)
287
288         # terminate
289         sys.exit(0)
290
291
292     if cmd.name not in ['create', 'restore'] and cmd.requires_wallet and not storage.file_exists:
293         print_msg("Error: Wallet file not found.")
294         print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
295         sys.exit(0)
296
297
298     if cmd.requires_wallet:
299         wallet = Wallet(storage)
300     else:
301         wallet = None
302
303
304     # important warning
305     if cmd.name in ['dumpprivkey', 'dumpprivkeys']:
306         print_stderr("WARNING: ALL your private keys are secret.")
307         print_stderr("Exposing a single private key can compromise your entire wallet!")
308         print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
309
310     # commands needing password
311     if cmd.requires_password:
312         if wallet.seed == '':
313             seed = ''
314             password = None
315         elif wallet.use_encryption:
316             password = prompt_password('Password:', False)
317             if not password:
318                 print_msg("Error: Password required")
319                 sys.exit(1)
320             # check password
321             try:
322                 seed = wallet.get_seed(password)
323             except Exception:
324                 print_msg("Error: This password does not decode this wallet.")
325                 sys.exit(1)
326         else:
327             password = None
328             seed = wallet.get_seed(None)
329     else:
330         password = None
331
332     # add missing arguments, do type conversions
333     if cmd.name == 'importprivkey':
334         # See if they specificed a key on the cmd line, if not prompt
335         if len(args) == 1:
336             args.append(prompt_password('Enter PrivateKey (will not echo):', False))
337
338     elif cmd.name == 'signrawtransaction':
339         args = [cmd, args[1], json.loads(args[2]) if len(args) > 2 else [] ]
340
341     elif cmd.name == 'createmultisig':
342         args = [cmd, int(args[1]), json.loads(args[2])]
343
344     elif cmd.name == 'createrawtransaction':
345         args = [cmd, json.loads(args[1]), json.loads(args[2])]
346
347     elif cmd.name == 'listaddresses':
348         args = [cmd, options.show_all, options.show_labels]
349
350     elif cmd.name in ['payto', 'mktx']:
351         domain = [options.from_addr] if options.from_addr else None
352         args = ['mktx', args[1], Decimal(args[2]), Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain]
353
354     elif cmd.name in ['paytomany', 'mksendmanytx']:
355         domain = [options.from_addr] if options.from_addr else None
356         outputs = []
357         for i in range(1, len(args), 2):
358             if len(args) < i+2:
359                 print_msg("Error: Mismatched arguments.")
360                 sys.exit(1)
361             outputs.append((args[i], Decimal(args[i+1])))
362         args = ['mksendmanytx', outputs, Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain]
363
364     elif cmd.name == 'help':
365         if len(args) < 2:
366             print_help(parser)
367
368     # check the number of arguments
369     if len(args) - 1 < cmd.min_args:
370         print_msg("Not enough arguments")
371         print_msg("Syntax:", cmd.syntax)
372         sys.exit(1)
373
374     if cmd.max_args >= 0 and len(args) - 1 > cmd.max_args:
375         print_msg("too many arguments", args)
376         print_msg("Syntax:", cmd.syntax)
377         sys.exit(1)
378
379     if cmd.max_args < 0:
380         if len(args) > cmd.min_args + 1:
381             message = ' '.join(args[cmd.min_args:])
382             print_msg("Warning: Final argument was reconstructed from several arguments:", repr(message))
383             args = args[0:cmd.min_args] + [message]
384
385
386
387     # run the command
388     if cmd.name == 'deseed':
389         if not wallet.seed:
390             print_msg("Error: This wallet has no seed")
391         else:
392             ns = wallet.storage.path + '.seedless'
393             print_msg("Warning: you are going to create a seedless wallet'\nIt will be saved in '%s'" % ns)
394             if raw_input("Are you sure you want to continue? (y/n) ") in ['y', 'Y', 'yes']:
395                 wallet.storage.path = ns
396                 wallet.seed = ''
397                 wallet.storage.put('seed', '', True)
398                 wallet.use_encryption = False
399                 wallet.storage.put('use_encryption', wallet.use_encryption, True)
400                 for k in wallet.imported_keys.keys():
401                     wallet.imported_keys[k] = ''
402                 wallet.storage.put('imported_keys', wallet.imported_keys, True)
403                 print_msg("Done.")
404             else:
405                 print_msg("Action canceled.")
406
407     elif cmd.name == 'getconfig':
408         key = args[1]
409         out = config.get(key)
410         print_msg(out)
411
412     elif cmd.name == 'setconfig':
413         key, value = args[1:3]
414         try:
415             value = ast.literal_eval(value)
416         except:
417             pass
418         config.set_key(key, value, True)
419         print_msg(True)
420
421     elif cmd.name == 'password':
422         new_password = prompt_password('New password:')
423         wallet.update_password(password, new_password)
424
425     else:
426         run_command(cmd, password, args)
427
428
429     time.sleep(0.1)
430     sys.exit(0)