Transferred help text into README.md file.
[btcspy.git] / lib / wallet.py
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 import sys
20 import base64
21 import os
22 import re
23 import hashlib
24 import copy
25 import operator
26 import ast
27 import threading
28 import random
29 #import aes
30 import Queue
31 import time
32 import math
33
34 from util import print_msg, print_error, format_satoshis
35 #from bitcoin import *
36 from account import *
37 from transaction import Transaction
38 #from plugins import run_hook
39 #import bitcoin
40 #from synchronizer import WalletSynchronizer
41
42 COINBASE_MATURITY = 100
43 DUST_THRESHOLD = 5430
44
45 # internal ID for imported account
46 IMPORTED_ACCOUNT = '/x'
47
48
49
50 from version import *
51
52
53 class WalletStorage:
54
55 def __init__(self, config):
56 self.lock = threading.Lock()
57 self.config = config
58 self.data = {}
59 self.file_exists = False
60 self.path = self.init_path(config)
61 print_error( "wallet path", self.path )
62 if self.path:
63 self.read(self.path)
64
65
66 def init_path(self, config):
67 """Set the path of the wallet."""
68
69 # command line -w option
70 path = config.get('wallet_path')
71 if path:
72 return path
73
74 # path in config file
75 path = config.get('default_wallet_path')
76 if path:
77 return path
78
79 # default path
80 dirpath = os.path.join(config.path, "wallets")
81 if not os.path.exists(dirpath):
82 os.mkdir(dirpath)
83
84 new_path = os.path.join(config.path, "wallets", "default_wallet")
85
86 # default path in pre 1.9 versions
87 old_path = os.path.join(config.path, "electrum.dat")
88 if os.path.exists(old_path) and not os.path.exists(new_path):
89 os.rename(old_path, new_path)
90
91 return new_path
92
93
94 def read(self, path):
95 """Read the contents of the wallet file."""
96 try:
97 with open(self.path, "r") as f:
98 data = f.read()
99 except IOError:
100 return
101 try:
102 d = ast.literal_eval( data ) #parse raw data from reading wallet file
103 except Exception:
104 raise IOError("Cannot read wallet file.")
105
106 self.data = d
107 self.file_exists = True
108
109
110 def get(self, key, default=None):
111 v = self.data.get(key)
112 if v is None:
113 v = default
114 return v
115
116 def put(self, key, value, save = True):
117
118 with self.lock:
119 if value is not None:
120 self.data[key] = value
121 elif key in self.data:
122 self.data.pop(key)
123 if save:
124 self.write()
125
126
127
128 class Abstract_Wallet:
129
130 def __init__(self, storage):
131
132 self.storage = storage
133 self.electrum_version = ELECTRUM_VERSION
134 self.gap_limit_for_change = 3 # constant
135 # saved fields
136 self.seed_version = storage.get('seed_version', NEW_SEED_VERSION)
137 self.gap_limit = storage.get('gap_limit', 5)
138 self.use_change = storage.get('use_change',True)
139 self.use_encryption = storage.get('use_encryption', False)
140 self.seed = storage.get('seed', '') # encrypted
141 self.labels = storage.get('labels', {})
142 self.frozen_addresses = storage.get('frozen_addresses',[])
143 self.addressbook = storage.get('contacts', [])
144
145 self.history = storage.get('addr_history',{}) # address -> list(txid, height)
146
147 self.fee = int(storage.get('fee_per_kb', 10000))
148
149 self.master_public_keys = storage.get('master_public_keys',{})
150 self.master_private_keys = storage.get('master_private_keys', {})
151
152 self.next_addresses = storage.get('next_addresses',{})
153
154
155 self.load_accounts()
156
157 self.transactions = {}
158 tx_list = self.storage.get('transactions',{})
159 for k,v in tx_list.items():
160 try:
161 tx = Transaction(v)
162 except Exception:
163 print_msg("Warning: Cannot deserialize transactions. skipping")
164 continue
165
166 self.add_extra_addresses(tx)
167 self.transactions[k] = tx
168
169 for h,tx in self.transactions.items():
170 if not self.check_new_tx(h, tx):
171 print_error("removing unreferenced tx", h)
172 self.transactions.pop(h)
173
174
175 # not saved
176 self.prevout_values = {} # my own transaction outputs
177 self.spent_outputs = []
178
179 # spv
180 self.verifier = None
181
182 # there is a difference between wallet.up_to_date and interface.is_up_to_date()
183 # interface.is_up_to_date() returns true when all requests have been answered and processed
184 # wallet.up_to_date is true when the wallet is synchronized (stronger requirement)
185
186 self.up_to_date = False
187 self.lock = threading.Lock()
188 self.transaction_lock = threading.Lock()
189 self.tx_event = threading.Event()
190
191 for tx_hash, tx in self.transactions.items():
192 self.update_tx_outputs(tx_hash)
193
194
195 def add_extra_addresses(self, tx):
196 h = tx.hash()
197 # find the address corresponding to pay-to-pubkey inputs
198 tx.add_extra_addresses(self.transactions)
199 for o in tx.d.get('outputs'):
200 if o.get('is_pubkey'):
201 for tx2 in self.transactions.values():
202 tx2.add_extra_addresses({h:tx})
203
204
205 def load_accounts(self):
206 self.accounts = {}
207 self.imported_keys = self.storage.get('imported_keys',{})
208
209 d = self.storage.get('accounts', {})
210 for k, v in d.items():
211 if k == 0:
212 v['mpk'] = self.storage.get('master_public_key')
213 self.accounts[k] = OldAccount(v)
214 elif v.get('imported'):
215 self.accounts[k] = ImportedAccount(v)
216 elif v.get('xpub3'):
217 self.accounts[k] = BIP32_Account_2of3(v)
218 elif v.get('xpub2'):
219 self.accounts[k] = BIP32_Account_2of2(v)
220 elif v.get('xpub'):
221 self.accounts[k] = BIP32_Account(v)
222 elif v.get('pending'):
223 self.accounts[k] = PendingAccount(v)
224 else:
225 print_error("cannot load account", v)
226
227
228 def can_create_accounts(self):
229 return False
230
231 def set_up_to_date(self,b):
232 with self.lock: self.up_to_date = b
233
234 def is_up_to_date(self):
235 with self.lock: return self.up_to_date
236
237
238 def update(self):
239 self.up_to_date = False
240 while not self.is_up_to_date():
241 time.sleep(0.1)
242
243 def is_imported(self, addr):
244 account = self.accounts.get(IMPORTED_ACCOUNT)
245 if account:
246 return addr in account.get_addresses(0)
247 else:
248 return False
249
250 def has_imported_keys(self):
251 account = self.accounts.get(IMPORTED_ACCOUNT)
252 return account is not None
253
254 def set_label(self, name, text = None):
255 changed = False
256 old_text = self.labels.get(name)
257 if text:
258 if old_text != text:
259 self.labels[name] = text
260 changed = True
261 else:
262 if old_text:
263 self.labels.pop(name)
264 changed = True
265
266 if changed:
267 self.storage.put('labels', self.labels, True)
268
269 run_hook('set_label', name, text, changed)
270 return changed
271
272 def addresses(self, include_change = True, _next=True):
273 o = []
274 for a in self.accounts.keys():
275 o += self.get_account_addresses(a, include_change)
276
277 if _next:
278 for addr in self.next_addresses.values():
279 if addr not in o:
280 o += [addr]
281 return o
282
283
284 def is_mine(self, address):
285 return address in self.addresses(True)
286
287
288 def is_change(self, address):
289 if not self.is_mine(address): return False
290 acct, s = self.get_address_index(address)
291 if s is None: return False
292 return s[0] == 1
293
294
295 def get_address_index(self, address):
296
297 for account in self.accounts.keys():
298 for for_change in [0,1]:
299 addresses = self.accounts[account].get_addresses(for_change)
300 for addr in addresses:
301 if address == addr:
302 return account, (for_change, addresses.index(addr))
303
304 for k,v in self.next_addresses.items():
305 if v == address:
306 return k, (0,0)
307
308 raise Exception("Address not found", address)
309
310
311 def getpubkeys(self, addr):
312 assert is_valid(addr) and self.is_mine(addr)
313 account, sequence = self.get_address_index(addr)
314 a = self.accounts[account]
315 return a.get_pubkeys( sequence )
316
317
318 def get_private_key(self, address, password):
319 if self.is_watching_only():
320 return []
321 account_id, sequence = self.get_address_index(address)
322 return self.accounts[account_id].get_private_key(sequence, self, password)
323
324
325 def get_public_keys(self, address):
326 account_id, sequence = self.get_address_index(address)
327 return self.accounts[account_id].get_pubkeys(sequence)
328
329
330 def sign_message(self, address, message, password):
331 keys = self.get_private_key(address, password)
332 assert len(keys) == 1
333 sec = keys[0]
334 key = regenerate_key(sec)
335 compressed = is_compressed(sec)
336 return key.sign_message(message, compressed, address)
337
338
339
340 def decrypt_message(self, pubkey, message, password):
341 address = public_key_to_bc_address(pubkey.decode('hex'))
342 keys = self.get_private_key(address, password)
343 secret = keys[0]
344 ec = regenerate_key(secret)
345 decrypted = ec.decrypt_message(message)
346 return decrypted
347
348
349
350 def is_found(self):
351 return self.history.values() != [[]] * len(self.history)
352
353
354 def get_tx_value(self, tx, account=None):
355 domain = self.get_account_addresses(account)
356 return tx.get_value(domain, self.prevout_values)
357
358
359 def update_tx_outputs(self, tx_hash):
360 tx = self.transactions.get(tx_hash)
361
362 for i, (addr, value) in enumerate(tx.outputs):
363 key = tx_hash+ ':%d'%i
364 self.prevout_values[key] = value
365
366 for item in tx.inputs:
367 if self.is_mine(item.get('address')):
368 key = item['prevout_hash'] + ':%d'%item['prevout_n']
369 self.spent_outputs.append(key)
370
371
372 def get_addr_balance(self, address):
373 #assert self.is_mine(address)
374 h = self.history.get(address,[])
375 if h == ['*']: return 0,0
376 c = u = 0
377 received_coins = [] # list of coins received at address
378
379 for tx_hash, tx_height in h:
380 tx = self.transactions.get(tx_hash)
381 if not tx: continue
382
383 for i, (addr, value) in enumerate(tx.outputs):
384 if addr == address:
385 key = tx_hash + ':%d'%i
386 received_coins.append(key)
387
388 for tx_hash, tx_height in h:
389 tx = self.transactions.get(tx_hash)
390 if not tx: continue
391 v = 0
392
393 for item in tx.inputs:
394 addr = item.get('address')
395 if addr == address:
396 key = item['prevout_hash'] + ':%d'%item['prevout_n']
397 value = self.prevout_values.get( key )
398 if key in received_coins:
399 v -= value
400
401 for i, (addr, value) in enumerate(tx.outputs):
402 key = tx_hash + ':%d'%i
403 if addr == address:
404 v += value
405
406 if tx_height:
407 c += v
408 else:
409 u += v
410 return c, u
411
412
413 def get_account_name(self, k):
414 return self.labels.get(k, self.accounts[k].get_name(k))
415
416
417 def get_account_names(self):
418 account_names = {}
419 for k in self.accounts.keys():
420 account_names[k] = self.get_account_name(k)
421 return account_names
422
423
424 def get_account_addresses(self, a, include_change=True):
425 if a is None:
426 o = self.addresses(True)
427 elif a in self.accounts:
428 ac = self.accounts[a]
429 o = ac.get_addresses(0)
430 if include_change: o += ac.get_addresses(1)
431 return o
432
433
434 def get_account_balance(self, account):
435 return self.get_balance(self.get_account_addresses(account))
436
437 def get_frozen_balance(self):
438 return self.get_balance(self.frozen_addresses)
439
440 def get_balance(self, domain=None):
441 if domain is None: domain = self.addresses(True)
442 cc = uu = 0
443 for addr in domain:
444 c, u = self.get_addr_balance(addr)
445 cc += c
446 uu += u
447 return cc, uu
448
449
450 def get_unspent_coins(self, domain=None):
451 coins = []
452 if domain is None: domain = self.addresses(True)
453 for addr in domain:
454 h = self.history.get(addr, [])
455 if h == ['*']: continue
456 for tx_hash, tx_height in h:
457 tx = self.transactions.get(tx_hash)
458 if tx is None: raise Exception("Wallet not synchronized")
459 is_coinbase = tx.inputs[0].get('prevout_hash') == '0'*64
460 for o in tx.d.get('outputs'):
461 output = o.copy()
462 if output.get('address') != addr: continue
463 key = tx_hash + ":%d" % output.get('prevout_n')
464 if key in self.spent_outputs: continue
465 output['prevout_hash'] = tx_hash
466 output['height'] = tx_height
467 output['coinbase'] = is_coinbase
468 coins.append((tx_height, output))
469
470 # sort by age
471 if coins:
472 coins = sorted(coins)
473 if coins[-1][0] != 0:
474 while coins[0][0] == 0:
475 coins = coins[1:] + [ coins[0] ]
476 return [x[1] for x in coins]
477
478
479 def get_history(self, address):
480 with self.lock:
481 return self.history.get(address)
482
483
484 def get_tx_history(self, account=None):
485 # if not self.verifier:
486 # return []
487
488 with self.transaction_lock:
489 history = self.transactions.items()
490 #history.sort(key = lambda x: self.verifier.get_txpos(x[0]))
491 result = []
492
493 balance = 0
494 for tx_hash, tx in history:
495 is_relevant, is_mine, v, fee = self.get_tx_value(tx, account)
496 if v is not None: balance += v
497
498 c, u = self.get_account_balance(account)
499
500 if balance != c+u:
501 result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
502
503 balance = c + u - balance
504 for tx_hash, tx in history:
505 is_relevant, is_mine, value, fee = self.get_tx_value(tx, account)
506 if not is_relevant:
507 continue
508 if value is not None:
509 balance += value
510
511 conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
512 result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
513
514 return result
515
516
517 def get_label(self, tx_hash):
518 label = self.labels.get(tx_hash)
519 is_default = (label == '') or (label is None)
520 if is_default: label = self.get_default_label(tx_hash)
521 return label, is_default
522
523
524 def get_default_label(self, tx_hash):
525 tx = self.transactions.get(tx_hash)
526 default_label = ''
527 if tx:
528 is_relevant, is_mine, _, _ = self.get_tx_value(tx)
529 if is_mine:
530 for o in tx.outputs:
531 o_addr, _ = o
532 if not self.is_mine(o_addr):
533 try:
534 default_label = self.labels[o_addr]
535 except KeyError:
536 default_label = '>' + o_addr
537 break
538 else:
539 default_label = '(internal)'
540 else:
541 for o in tx.outputs:
542 o_addr, _ = o
543 if self.is_mine(o_addr) and not self.is_change(o_addr):
544 break
545 else:
546 for o in tx.outputs:
547 o_addr, _ = o
548 if self.is_mine(o_addr):
549 break
550 else:
551 o_addr = None
552
553 if o_addr:
554 dest_label = self.labels.get(o_addr)
555 try:
556 default_label = self.labels[o_addr]
557 except KeyError:
558 default_label = '<' + o_addr
559
560 return default_label
561
562
563 def sign_transaction(self, tx, keypairs, password):
564 tx.sign(keypairs)
565 run_hook('sign_transaction', tx, password)
566
567
568 def check_new_tx(self, tx_hash, tx):
569 # 1 check that tx is referenced in addr_history.
570 addresses = []
571 for addr, hist in self.history.items():
572 if hist == ['*']:continue
573 for txh, height in hist:
574 if txh == tx_hash:
575 addresses.append(addr)
576
577 if not addresses:
578 return False
579
580 # 2 check that referencing addresses are in the tx
581 for addr in addresses:
582 if not tx.has_address(addr):
583 return False
584
585 return True
586
587
588 class Deterministic_Wallet(Abstract_Wallet):
589
590 def __init__(self, storage):
591 Abstract_Wallet.__init__(self, storage)
592
593 def has_seed(self):
594 return self.seed != ''
595
596 def is_deterministic(self):
597 return True
598
599 def is_watching_only(self):
600 return not self.has_seed()
601
602 def get_seed(self, password):
603 return pw_decode(self.seed, password)
604
605 def get_mnemonic(self, password):
606 return self.get_seed(password)
607
608 def num_unused_trailing_addresses(self, addresses):
609 k = 0
610 for a in addresses[::-1]:
611 if self.history.get(a):break
612 k = k + 1
613 return k
614
615 def min_acceptable_gap(self):
616 # fixme: this assumes wallet is synchronized
617 n = 0
618 nmax = 0
619
620 for account in self.accounts.values():
621 addresses = account.get_addresses(0)
622 k = self.num_unused_trailing_addresses(addresses)
623 for a in addresses[0:-k]:
624 if self.history.get(a):
625 n = 0
626 else:
627 n += 1
628 if n > nmax: nmax = n
629 return nmax + 1
630
631
632 def address_is_old(self, address):
633 age = -1
634 h = self.history.get(address, [])
635 return False
636 # if h == ['*']:
637 # return True
638 # for tx_hash, tx_height in h:
639 # if tx_height == 0:
640 # tx_age = 0
641 # else:
642 # tx_age = self.network.get_local_height() - tx_height + 1
643 # if tx_age > age:
644 # age = tx_age
645 # return age > 2
646
647
648 class OldWallet(Deterministic_Wallet):
649
650 def get_seed(self, password):
651 seed = pw_decode(self.seed, password).encode('utf8')
652 return seed
653
654 def check_password(self, password):
655 seed = self.get_seed(password)
656 self.accounts[0].check_seed(seed)
657
658 def get_mnemonic(self, password):
659 import mnemonic
660 s = self.get_seed(password)
661 return ' '.join(mnemonic.mn_encode(s))
662
663 # former WalletFactory
664 class Wallet(object):
665
666 def __new__(self, storage):
667 config = storage.config
668
669 if not storage.file_exists:
670 print ("Wallet does no exist")
671 sys.exit(1)
672
673 seed_version = storage.get('seed_version')
674 if not seed_version:
675 seed_version = OLD_SEED_VERSION if len(storage.get('master_public_key')) == 128 else NEW_SEED_VERSION
676
677 if seed_version == OLD_SEED_VERSION:
678 return OldWallet(storage)
679 elif seed_version == NEW_SEED_VERSION:
680 return NewWallet(storage)
681 else:
682 msg = "This wallet seed is not supported."
683 if seed_version in [5]:
684 msg += "\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version
685 print msg
686 sys.exit(1)
687
688