fix update_password: save use_encryption after seed
[electrum-nvc.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
33 from util import print_msg, print_error, user_dir, format_satoshis
34 from bitcoin import *
35
36
37 # AES encryption
38 EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s))
39 DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e))
40
41 def pw_encode(s, password):
42     if password:
43         secret = Hash(password)
44         return EncodeAES(secret, s)
45     else:
46         return s
47
48 def pw_decode(s, password):
49     if password is not None:
50         secret = Hash(password)
51         try:
52             d = DecodeAES(secret, s)
53         except:
54             raise BaseException('Invalid password')
55         return d
56     else:
57         return s
58
59
60
61
62
63 from version import ELECTRUM_VERSION, SEED_VERSION
64
65
66 class Wallet:
67     def __init__(self, config={}):
68
69         self.config = config
70         self.electrum_version = ELECTRUM_VERSION
71         self.gap_limit_for_change = 3 # constant
72
73         # saved fields
74         self.seed_version          = config.get('seed_version', SEED_VERSION)
75         self.gap_limit             = config.get('gap_limit', 5)
76         self.use_change            = config.get('use_change',True)
77         self.fee                   = int(config.get('fee_per_kb',20000))
78         self.num_zeros             = int(config.get('num_zeros',0))
79         self.use_encryption        = config.get('use_encryption', False)
80         self.seed                  = config.get('seed', '')               # encrypted
81         self.labels                = config.get('labels', {})
82         self.frozen_addresses      = config.get('frozen_addresses',[])
83         self.prioritized_addresses = config.get('prioritized_addresses',[])
84         self.addressbook           = config.get('contacts', [])
85         self.imported_keys         = config.get('imported_keys',{})
86         self.history               = config.get('addr_history',{})        # address -> list(txid, height)
87         self.accounts              = config.get('accounts', {})   # this should not include public keys
88
89         self.SequenceClass = ElectrumSequence
90         self.sequences = {}
91         self.sequences[0] = self.SequenceClass(self.config.get('master_public_key'))
92
93         if self.accounts.get(0) is None:
94             self.accounts[0] = { 0:[], 1:[], 'name':'Main account' }
95
96         self.transactions = {}
97         tx = config.get('transactions',{})
98         try:
99             for k,v in tx.items(): self.transactions[k] = Transaction(v)
100         except:
101             print_msg("Warning: Cannot deserialize transactions. skipping")
102         
103         # not saved
104         self.prevout_values = {}     # my own transaction outputs
105         self.spent_outputs = []
106
107         # spv
108         self.verifier = None
109
110         # there is a difference between wallet.up_to_date and interface.is_up_to_date()
111         # interface.is_up_to_date() returns true when all requests have been answered and processed
112         # wallet.up_to_date is true when the wallet is synchronized (stronger requirement)
113         
114         self.up_to_date = False
115         self.lock = threading.Lock()
116         self.transaction_lock = threading.Lock()
117         self.tx_event = threading.Event()
118
119         if self.seed_version != SEED_VERSION:
120             raise ValueError("This wallet seed is deprecated. Please run upgrade.py for a diagnostic.")
121
122         for tx_hash in self.transactions.keys():
123             self.update_tx_outputs(tx_hash)
124
125
126     def set_up_to_date(self,b):
127         with self.lock: self.up_to_date = b
128
129     def is_up_to_date(self):
130         with self.lock: return self.up_to_date
131
132     def update(self):
133         self.up_to_date = False
134         self.interface.poke('synchronizer')
135         while not self.is_up_to_date(): time.sleep(0.1)
136
137     def import_key(self, sec, password):
138         # check password
139         seed = self.decode_seed(password)
140         address = address_from_private_key(sec)
141
142         if self.is_mine(address):
143             raise BaseException('Address already in wallet')
144         
145         # store the originally requested keypair into the imported keys table
146         self.imported_keys[address] = pw_encode(sec, password )
147         self.config.set_key('imported_keys', self.imported_keys, True)
148         return address
149         
150
151     def init_seed(self, seed):
152         if self.seed: raise BaseException("a seed exists")
153         if not seed: 
154             seed = random_seed(128)
155         self.seed = seed
156
157     def save_seed(self):
158         self.config.set_key('seed', self.seed, True)
159         self.config.set_key('seed_version', self.seed_version, True)
160         mpk = self.SequenceClass.mpk_from_seed(self.seed)
161         self.init_sequence(mpk)
162
163
164     def init_sequence(self, mpk):
165         self.config.set_key('master_public_key', mpk, True)
166         self.sequences[0] = self.SequenceClass(mpk)
167         self.accounts[0] = { 0:[], 1:[], 'name':'Main account' }
168         self.config.set_key('accounts', self.accounts, True)
169
170
171     def addresses(self, include_change = True):
172         o = self.get_account_addresses(-1, include_change)
173         for a in self.accounts.keys():
174             o += self.get_account_addresses(a, include_change)
175         return o
176
177
178     def is_mine(self, address):
179         return address in self.addresses(True)
180
181     def is_change(self, address):
182         if not self.is_mine(address): return False
183         if address in self.imported_keys.keys(): return False
184         acct, s = self.get_address_index(address)
185         return s[0] == 1
186
187     def get_master_public_key(self):
188         return self.config.get("master_public_key")
189
190     def get_address_index(self, address):
191         if address in self.imported_keys.keys():
192             return -1, None
193         for account in self.accounts.keys():
194             for for_change in [0,1]:
195                 addresses = self.accounts[account][for_change]
196                 for addr in addresses:
197                     if address == addr:
198                         return account, (for_change, addresses.index(addr))
199         raise BaseException("not found")
200         
201
202     def get_public_key(self, address):
203         account, sequence = self.get_address_index(address)
204         return self.sequences[account].get_pubkey( sequence )
205
206
207     def decode_seed(self, password):
208         seed = pw_decode(self.seed, password)
209         self.sequences[0].check_seed(seed)
210         return seed
211         
212     def get_private_key(self, address, password):
213         return self.get_private_keys([address], password).get(address)
214
215     def get_private_keys(self, addresses, password):
216         if not self.seed: return {}
217         # decode seed in any case, in order to test the password
218         seed = self.decode_seed(password)
219         out = {}
220         l_sequences = []
221         l_addresses = []
222         for address in addresses:
223             if address in self.imported_keys.keys():
224                 out[address] = pw_decode( self.imported_keys[address], password )
225             else:
226                 account, sequence = self.get_address_index(address)
227                 if account == 0:
228                     l_sequences.append(sequence)
229                     l_addresses.append(address)
230
231         pk = self.sequences[0].get_private_keys(l_sequences, seed)
232         for i, address in enumerate(l_addresses): out[address] = pk[i]                     
233         return out
234
235
236     def signrawtransaction(self, tx, input_info, private_keys, password):
237         unspent_coins = self.get_unspent_coins()
238         seed = self.decode_seed(password)
239
240         # convert private_keys to dict 
241         pk = {}
242         for sec in private_keys:
243             address = address_from_private_key(sec)
244             pk[address] = sec
245         private_keys = pk
246
247         for txin in tx.inputs:
248             # convert to own format
249             txin['tx_hash'] = txin['prevout_hash']
250             txin['index'] = txin['prevout_n']
251
252             for item in input_info:
253                 if item.get('txid') == txin['tx_hash'] and item.get('vout') == txin['index']:
254                     txin['raw_output_script'] = item['scriptPubKey']
255                     txin['redeemScript'] = item.get('redeemScript')
256                     txin['KeyID'] = item.get('KeyID')
257                     break
258             else:
259                 for item in unspent_coins:
260                     if txin['tx_hash'] == item['tx_hash'] and txin['index'] == item['index']:
261                         txin['raw_output_script'] = item['raw_output_script']
262                         break
263                 else:
264                     # if neither, we might want to get it from the server..
265                     raise
266
267             # find the address:
268             if txin.get('KeyID'):
269                 account, name, sequence = txin.get('KeyID')
270                 if name != 'Electrum': continue
271                 sec = self.sequences[account].get_private_key(sequence, seed)
272                 addr = self.sequences[account].get_address(sequence)
273                 txin['address'] = addr
274                 private_keys[addr] = sec
275
276             elif txin.get("redeemScript"):
277                 txin['address'] = hash_160_to_bc_address(hash_160(txin.get("redeemScript").decode('hex')), 5)
278
279             elif txin.get("raw_output_script"):
280                 import deserialize
281                 addr = deserialize.get_address_from_output_script(txin.get("raw_output_script").decode('hex'))
282                 sec = self.get_private_key(addr, password)
283                 if sec: 
284                     private_keys[addr] = sec
285                     txin['address'] = addr
286
287         tx.sign( private_keys )
288
289     def sign_message(self, address, message, password):
290         sec = self.get_private_key(address, password)
291         key = regenerate_key(sec)
292         compressed = is_compressed(sec)
293         return key.sign_message(message, compressed, address)
294
295
296     def create_new_address(self, account, for_change):
297         addresses = self.accounts[account][for_change]
298         n = len(addresses)
299         address = self.get_new_address( account, for_change, n)
300         self.accounts[account][for_change].append(address)
301         self.history[address] = []
302         print_msg(address)
303         return address
304         
305
306     def get_new_address(self, account, for_change, n):
307         return self.sequences[account].get_address((for_change, n))
308         print address
309         return address
310
311     def change_gap_limit(self, value):
312         if value >= self.gap_limit:
313             self.gap_limit = value
314             self.save()
315             self.interface.poke('synchronizer')
316             return True
317
318         elif value >= self.min_acceptable_gap():
319             for key, account in self.accounts.items():
320                 addresses = account[0]
321                 k = self.num_unused_trailing_addresses(addresses)
322                 n = len(addresses) - k + value
323                 addresses = addresses[0:n]
324                 self.accounts[key][0] = addresses
325
326             self.gap_limit = value
327             self.save()
328             return True
329         else:
330             return False
331
332     def num_unused_trailing_addresses(self, addresses):
333         k = 0
334         for a in addresses[::-1]:
335             if self.history.get(a):break
336             k = k + 1
337         return k
338
339     def min_acceptable_gap(self):
340         # fixme: this assumes wallet is synchronized
341         n = 0
342         nmax = 0
343
344         for account in self.accounts.values():
345             addresses = account[0]
346             k = self.num_unused_trailing_addresses(addresses)
347             for a in addresses[0:-k]:
348                 if self.history.get(a):
349                     n = 0
350                 else:
351                     n += 1
352                     if n > nmax: nmax = n
353         return nmax + 1
354
355
356     def address_is_old(self, address):
357         age = -1
358         h = self.history.get(address, [])
359         if h == ['*']:
360             return True
361         for tx_hash, tx_height in h:
362             if tx_height == 0:
363                 tx_age = 0
364             else: 
365                 tx_age = self.verifier.height - tx_height + 1
366             if tx_age > age:
367                 age = tx_age
368         return age > 2
369
370
371     def synchronize_sequence(self, account, for_change):
372         limit = self.gap_limit_for_change if for_change else self.gap_limit
373         addresses = self.accounts[account][for_change]
374         new_addresses = []
375         while True:
376             if len(addresses) < limit:
377                 new_addresses.append( self.create_new_address(account, for_change) )
378                 continue
379             if map( lambda a: self.address_is_old(a), addresses[-limit:] ) == limit*[False]:
380                 break
381             else:
382                 new_addresses.append( self.create_new_address(account, for_change) )
383         return new_addresses
384         
385
386     def synchronize_account(self, account):
387         new = []
388         new += self.synchronize_sequence(account, 0)
389         new += self.synchronize_sequence(account, 1)
390         return new
391
392     def synchronize(self):
393         new = []
394         for account in self.accounts.keys():
395             new += self.synchronize_account(account)
396         return new
397
398
399     def is_found(self):
400         return self.history.values() != [[]] * len(self.history) 
401
402
403     def fill_addressbook(self):
404         for tx_hash, tx in self.transactions.items():
405             is_relevant, is_send, _, _ = self.get_tx_value(tx)
406             if is_send:
407                 for addr, v in tx.outputs:
408                     if not self.is_mine(addr) and addr not in self.addressbook:
409                         self.addressbook.append(addr)
410         # redo labels
411         # self.update_tx_labels()
412
413     def get_num_tx(self, address):
414         n = 0 
415         for tx in self.transactions.values():
416             if address in map(lambda x:x[0], tx.outputs): n += 1
417         return n
418
419
420     def get_address_flags(self, addr):
421         flags = "C" if self.is_change(addr) else "I" if addr in self.imported_keys.keys() else "-" 
422         flags += "F" if addr in self.frozen_addresses else "P" if addr in self.prioritized_addresses else "-"
423         return flags
424         
425
426     def get_tx_value(self, tx, account=None):
427         domain = self.get_account_addresses(account)
428         return tx.get_value(domain, self.prevout_values)
429
430     
431     def update_tx_outputs(self, tx_hash):
432         tx = self.transactions.get(tx_hash)
433
434         for i, (addr, value) in enumerate(tx.outputs):
435             key = tx_hash+ ':%d'%i
436             self.prevout_values[key] = value
437
438         for item in tx.inputs:
439             if self.is_mine(item.get('address')):
440                 key = item['prevout_hash'] + ':%d'%item['prevout_n']
441                 self.spent_outputs.append(key)
442
443
444     def get_addr_balance(self, address):
445         assert self.is_mine(address)
446         h = self.history.get(address,[])
447         if h == ['*']: return 0,0
448         c = u = 0
449         received_coins = []   # list of coins received at address
450
451         for tx_hash, tx_height in h:
452             tx = self.transactions.get(tx_hash)
453             if not tx: continue
454
455             for i, (addr, value) in enumerate(tx.outputs):
456                 if addr == address:
457                     key = tx_hash + ':%d'%i
458                     received_coins.append(key)
459
460         for tx_hash, tx_height in h:
461             tx = self.transactions.get(tx_hash)
462             if not tx: continue
463             v = 0
464
465             for item in tx.inputs:
466                 addr = item.get('address')
467                 if addr == address:
468                     key = item['prevout_hash']  + ':%d'%item['prevout_n']
469                     value = self.prevout_values.get( key )
470                     if key in received_coins: 
471                         v -= value
472
473             for i, (addr, value) in enumerate(tx.outputs):
474                 key = tx_hash + ':%d'%i
475                 if addr == address:
476                     v += value
477
478             if tx_height:
479                 c += v
480             else:
481                 u += v
482         return c, u
483
484
485     def get_accounts(self):
486         accounts = {}
487         for k, account in self.accounts.items():
488             accounts[k] = account.get('name')
489         if self.imported_keys:
490             accounts[-1] = 'Imported keys'
491         return accounts
492
493     def get_account_addresses(self, a, include_change=True):
494         if a is None:
495             o = self.addresses(True)
496         elif a == -1:
497             o = self.imported_keys.keys()
498         else:
499             ac = self.accounts[a]
500             o = ac[0][:]
501             if include_change: o += ac[1]
502         return o
503
504     def get_imported_balance(self):
505         cc = uu = 0
506         for addr in self.imported_keys.keys():
507             c, u = self.get_addr_balance(addr)
508             cc += c
509             uu += u
510         return cc, uu
511
512     def get_account_balance(self, account):
513         if account is None:
514             return self.get_balance()
515         elif account == -1:
516             return self.get_imported_balance()
517         
518         conf = unconf = 0
519         for addr in self.get_account_addresses(account): 
520             c, u = self.get_addr_balance(addr)
521             conf += c
522             unconf += u
523         return conf, unconf
524
525     def get_frozen_balance(self):
526         conf = unconf = 0
527         for addr in self.frozen_addresses:
528             c, u = self.get_addr_balance(addr)
529             conf += c
530             unconf += u
531         return conf, unconf
532
533         
534     def get_balance(self):
535         cc = uu = 0
536         for a in self.accounts.keys():
537             c, u = self.get_account_balance(a)
538             cc += c
539             uu += u
540         c, u = self.get_imported_balance()
541         cc += c
542         uu += u
543         return cc, uu
544
545
546     def get_unspent_coins(self, domain=None):
547         coins = []
548         if domain is None: domain = self.addresses(True)
549         for addr in domain:
550             h = self.history.get(addr, [])
551             if h == ['*']: continue
552             for tx_hash, tx_height in h:
553                 tx = self.transactions.get(tx_hash)
554                 if tx is None: raise BaseException("Wallet not synchronized")
555                 for output in tx.d.get('outputs'):
556                     if output.get('address') != addr: continue
557                     key = tx_hash + ":%d" % output.get('index')
558                     if key in self.spent_outputs: continue
559                     output['tx_hash'] = tx_hash
560                     coins.append(output)
561         return coins
562
563
564
565     def choose_tx_inputs( self, amount, fixed_fee, account = None ):
566         """ todo: minimize tx size """
567         total = 0
568         fee = self.fee if fixed_fee is None else fixed_fee
569         domain = self.get_account_addresses(account)
570         coins = []
571         prioritized_coins = []
572         for i in self.frozen_addresses:
573             if i in domain: domain.remove(i)
574
575         for i in self.prioritized_addresses:
576             if i in domain: domain.remove(i)
577
578         coins = self.get_unspent_coins(domain)
579         prioritized_coins = self.get_unspent_coins(self.prioritized_addresses)
580
581         inputs = []
582         coins = prioritized_coins + coins
583
584         for item in coins: 
585             addr = item.get('address')
586             v = item.get('value')
587             total += v
588             inputs.append( item )
589             fee = self.estimated_fee(inputs) if fixed_fee is None else fixed_fee
590             if total >= amount + fee: break
591         else:
592             inputs = []
593
594         return inputs, total, fee
595
596
597     def estimated_fee(self, inputs):
598         estimated_size =  len(inputs) * 180 + 80     # this assumes non-compressed keys
599         fee = self.fee * int(round(estimated_size/1024.))
600         if fee == 0: fee = self.fee
601         return fee
602
603
604     def add_tx_change( self, inputs, outputs, amount, fee, total, change_addr=None, account=0 ):
605         "add change to a transaction"
606         change_amount = total - ( amount + fee )
607         if change_amount != 0:
608             if not change_addr:
609                 if account is None: 
610                     # send change to one of the accounts involved in the tx
611                     address = inputs[0].get('address')
612                     account, _ = self.get_address_index(address)
613
614                 if not self.use_change or account == -1:
615                     change_addr = inputs[-1]['address']
616                 else:
617                     change_addr = self.accounts[account][1][-self.gap_limit_for_change]
618
619             # Insert the change output at a random position in the outputs
620             posn = random.randint(0, len(outputs))
621             outputs[posn:posn] = [( change_addr,  change_amount)]
622         return outputs
623
624
625     def get_history(self, address):
626         with self.lock:
627             return self.history.get(address)
628
629
630     def get_status(self, h):
631         if not h: return None
632         if h == ['*']: return '*'
633         status = ''
634         for tx_hash, height in h:
635             status += tx_hash + ':%d:' % height
636         return hashlib.sha256( status ).digest().encode('hex')
637
638
639     def receive_tx_callback(self, tx_hash, tx, tx_height):
640
641         if not self.check_new_tx(tx_hash, tx):
642             # may happen due to pruning
643             print_error("received transaction that is no longer referenced in history", tx_hash)
644             return
645
646         with self.transaction_lock:
647             self.transactions[tx_hash] = tx
648             if self.verifier and tx_height>0: 
649                 self.verifier.add(tx_hash, tx_height)
650             self.update_tx_outputs(tx_hash)
651
652         self.save()
653
654
655     def receive_history_callback(self, addr, hist):
656
657         if not self.check_new_history(addr, hist):
658             raise BaseException("error: received history for %s is not consistent with known transactions"%addr)
659             
660         with self.lock:
661             self.history[addr] = hist
662             self.save()
663
664         if hist != ['*']:
665             for tx_hash, tx_height in hist:
666                 if tx_height>0:
667                     # add it in case it was previously unconfirmed
668                     if self.verifier: self.verifier.add(tx_hash, tx_height)
669
670
671     def get_tx_history(self, account=None):
672         with self.transaction_lock:
673             history = self.transactions.items()
674             history.sort(key = lambda x: self.verifier.get_txpos(x[0]))
675             result = []
676     
677             balance = 0
678             for tx_hash, tx in history:
679                 is_relevant, is_mine, v, fee = self.get_tx_value(tx, account)
680                 if v is not None: balance += v
681
682             c, u = self.get_account_balance(account)
683
684             if balance != c+u:
685                 result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
686
687             balance = c + u - balance
688             for tx_hash, tx in history:
689                 is_relevant, is_mine, value, fee = self.get_tx_value(tx, account)
690                 if not is_relevant:
691                     continue
692                 if value is not None:
693                     balance += value
694
695                 conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
696                 result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
697
698         return result
699
700
701     def get_label(self, tx_hash):
702         label = self.labels.get(tx_hash)
703         is_default = (label == '') or (label is None)
704         if is_default: label = self.get_default_label(tx_hash)
705         return label, is_default
706
707
708     def get_default_label(self, tx_hash):
709         tx = self.transactions.get(tx_hash)
710         default_label = ''
711         if tx:
712             is_relevant, is_mine, _, _ = self.get_tx_value(tx)
713             if is_mine:
714                 for o in tx.outputs:
715                     o_addr, _ = o
716                     if not self.is_mine(o_addr):
717                         try:
718                             default_label = self.labels[o_addr]
719                         except KeyError:
720                             default_label = o_addr
721                         break
722                 else:
723                     default_label = '(internal)'
724             else:
725                 for o in tx.outputs:
726                     o_addr, _ = o
727                     if self.is_mine(o_addr) and not self.is_change(o_addr):
728                         break
729                 else:
730                     for o in tx.outputs:
731                         o_addr, _ = o
732                         if self.is_mine(o_addr):
733                             break
734                     else:
735                         o_addr = None
736
737                 if o_addr:
738                     dest_label = self.labels.get(o_addr)
739                     try:
740                         default_label = self.labels[o_addr]
741                     except KeyError:
742                         default_label = o_addr
743
744         return default_label
745
746
747     def mktx(self, outputs, password, fee=None, change_addr=None, account=None ):
748         """
749         create a transaction
750         account parameter:
751            None means use all accounts
752            -1 means imported keys
753            0, 1, etc are seed accounts
754         """
755         
756         for address, x in outputs:
757             assert is_valid(address)
758
759         amount = sum( map(lambda x:x[1], outputs) )
760
761         inputs, total, fee = self.choose_tx_inputs( amount, fee, account )
762         if not inputs:
763             raise ValueError("Not enough funds")
764
765         outputs = self.add_tx_change(inputs, outputs, amount, fee, total, change_addr, account)
766
767         tx = Transaction.from_io(inputs, outputs)
768
769         pk_addresses = []
770         for i in range(len(tx.inputs)):
771             txin = tx.inputs[i]
772             address = txin['address']
773             if address in self.imported_keys.keys():
774                 pk_addresses.append(address)
775                 continue
776             account, sequence = self.get_address_index(address)
777             txin['KeyID'] = (account, 'Electrum', sequence) # used by the server to find the key
778             pk_addr, redeemScript = self.sequences[account].get_input_info(sequence)
779             if redeemScript: txin['redeemScript'] = redeemScript
780             pk_addresses.append(pk_addr)
781
782         # get all private keys at once.
783         if self.seed:
784             private_keys = self.get_private_keys(pk_addresses, password)
785             tx.sign(private_keys)
786
787         for address, x in outputs:
788             if address not in self.addressbook and not self.is_mine(address):
789                 self.addressbook.append(address)
790
791         return tx
792
793
794
795     def sendtx(self, tx):
796         # synchronous
797         h = self.send_tx(tx)
798         self.tx_event.wait()
799         return self.receive_tx(h)
800
801     def send_tx(self, tx):
802         # asynchronous
803         self.tx_event.clear()
804         self.interface.send([('blockchain.transaction.broadcast', [str(tx)])], 'synchronizer')
805         return tx.hash()
806
807     def receive_tx(self,tx_hash):
808         out = self.tx_result 
809         if out != tx_hash:
810             return False, "error: " + out
811         return True, out
812
813
814
815     def update_password(self, seed, old_password, new_password):
816         if new_password == '': new_password = None
817         # this will throw an exception if unicode cannot be converted
818         self.seed = pw_encode( seed, new_password)
819         self.config.set_key('seed', self.seed, True)
820         self.use_encryption = (new_password != None)
821         self.config.set_key('use_encryption', self.use_encryption,True)
822         for k in self.imported_keys.keys():
823             a = self.imported_keys[k]
824             b = pw_decode(a, old_password)
825             c = pw_encode(b, new_password)
826             self.imported_keys[k] = c
827         self.config.set_key('imported_keys', self.imported_keys, True)
828
829
830     def freeze(self,addr):
831         if self.is_mine(addr) and addr not in self.frozen_addresses:
832             self.unprioritize(addr)
833             self.frozen_addresses.append(addr)
834             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
835             return True
836         else:
837             return False
838
839     def unfreeze(self,addr):
840         if self.is_mine(addr) and addr in self.frozen_addresses:
841             self.frozen_addresses.remove(addr)
842             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
843             return True
844         else:
845             return False
846
847     def prioritize(self,addr):
848         if self.is_mine(addr) and addr not in self.prioritized_addresses:
849             self.unfreeze(addr)
850             self.prioritized_addresses.append(addr)
851             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
852             return True
853         else:
854             return False
855
856     def unprioritize(self,addr):
857         if self.is_mine(addr) and addr in self.prioritized_addresses:
858             self.prioritized_addresses.remove(addr)
859             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
860             return True
861         else:
862             return False
863
864     def save(self):
865         tx = {}
866         for k,v in self.transactions.items():
867             tx[k] = str(v)
868             
869         s = {
870             'use_change': self.use_change,
871             'fee_per_kb': self.fee,
872             'accounts': self.accounts,
873             'addr_history': self.history, 
874             'labels': self.labels,
875             'contacts': self.addressbook,
876             'num_zeros': self.num_zeros,
877             'frozen_addresses': self.frozen_addresses,
878             'prioritized_addresses': self.prioritized_addresses,
879             'gap_limit': self.gap_limit,
880             'transactions': tx,
881         }
882         for k, v in s.items():
883             self.config.set_key(k,v)
884         self.config.save()
885
886     def set_verifier(self, verifier):
887         self.verifier = verifier
888
889         # review transactions that are in the history
890         for addr, hist in self.history.items():
891             if hist == ['*']: continue
892             for tx_hash, tx_height in hist:
893                 if tx_height>0:
894                     # add it in case it was previously unconfirmed
895                     self.verifier.add(tx_hash, tx_height)
896
897
898         # if we are on a pruning server, remove unverified transactions
899         vr = self.verifier.transactions.keys() + self.verifier.verified_tx.keys()
900         for tx_hash in self.transactions.keys():
901             if tx_hash not in vr:
902                 self.transactions.pop(tx_hash)
903
904
905
906     def check_new_history(self, addr, hist):
907         
908         # check that all tx in hist are relevant
909         if hist != ['*']:
910             for tx_hash, height in hist:
911                 tx = self.transactions.get(tx_hash)
912                 if not tx: continue
913                 if not tx.has_address(addr):
914                     return False
915
916         # check that we are not "orphaning" a transaction
917         old_hist = self.history.get(addr,[])
918         if old_hist == ['*']: return True
919
920         for tx_hash, height in old_hist:
921             if tx_hash in map(lambda x:x[0], hist): continue
922             found = False
923             for _addr, _hist in self.history.items():
924                 if _addr == addr: continue
925                 if _hist == ['*']: continue
926                 _tx_hist = map(lambda x:x[0], _hist)
927                 if tx_hash in _tx_hist:
928                     found = True
929                     break
930
931             if not found:
932                 tx = self.transactions.get(tx_hash)
933                 # tx might not be there
934                 if not tx: continue
935                 
936                 # already verified?
937                 if self.verifier.get_height(tx_hash):
938                     continue
939                 # unconfirmed tx
940                 print_error("new history is orphaning transaction:", tx_hash)
941                 # check that all outputs are not mine, request histories
942                 ext_requests = []
943                 for _addr, _v in tx.outputs:
944                     # assert not self.is_mine(_addr)
945                     ext_requests.append( ('blockchain.address.get_history', [_addr]) )
946
947                 ext_h = self.interface.synchronous_get(ext_requests)
948                 print_error("sync:", ext_requests, ext_h)
949                 height = None
950                 for h in ext_h:
951                     if h == ['*']: continue
952                     for item in h:
953                         if item.get('tx_hash') == tx_hash:
954                             height = item.get('height')
955                 if height:
956                     print_error("found height for", tx_hash, height)
957                     self.verifier.add(tx_hash, height)
958                 else:
959                     print_error("removing orphaned tx from history", tx_hash)
960                     self.transactions.pop(tx_hash)
961
962         return True
963
964
965
966     def check_new_tx(self, tx_hash, tx):
967         # 1 check that tx is referenced in addr_history. 
968         addresses = []
969         for addr, hist in self.history.items():
970             if hist == ['*']:continue
971             for txh, height in hist:
972                 if txh == tx_hash: 
973                     addresses.append(addr)
974
975         if not addresses:
976             return False
977
978         # 2 check that referencing addresses are in the tx
979         for addr in addresses:
980             if not tx.has_address(addr):
981                 return False
982
983         return True
984
985
986
987
988 class WalletSynchronizer(threading.Thread):
989
990
991     def __init__(self, wallet, config):
992         threading.Thread.__init__(self)
993         self.daemon = True
994         self.wallet = wallet
995         self.interface = self.wallet.interface
996         self.interface.register_channel('synchronizer')
997         self.wallet.interface.register_callback('connected', lambda: self.wallet.set_up_to_date(False))
998         self.was_updated = True
999         self.running = False
1000         self.lock = threading.Lock()
1001
1002     def stop(self):
1003         with self.lock: self.running = False
1004         self.interface.poke('synchronizer')
1005
1006     def is_running(self):
1007         with self.lock: return self.running
1008
1009     
1010     def subscribe_to_addresses(self, addresses):
1011         messages = []
1012         for addr in addresses:
1013             messages.append(('blockchain.address.subscribe', [addr]))
1014         self.interface.send( messages, 'synchronizer')
1015
1016
1017     def run(self):
1018         with self.lock: self.running = True
1019
1020         requested_tx = []
1021         missing_tx = []
1022         requested_histories = {}
1023
1024         # request any missing transactions
1025         for history in self.wallet.history.values():
1026             if history == ['*']: continue
1027             for tx_hash, tx_height in history:
1028                 if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
1029                     missing_tx.append( (tx_hash, tx_height) )
1030         print_error("missing tx", missing_tx)
1031
1032         # wait until we are connected, in case the user is not connected
1033         while not self.interface.is_connected:
1034             time.sleep(1)
1035         
1036         # subscriptions
1037         self.subscribe_to_addresses(self.wallet.addresses(True))
1038
1039         while self.is_running():
1040             # 1. create new addresses
1041             new_addresses = self.wallet.synchronize()
1042
1043             # request missing addresses
1044             if new_addresses:
1045                 self.subscribe_to_addresses(new_addresses)
1046
1047             # request missing transactions
1048             for tx_hash, tx_height in missing_tx:
1049                 if (tx_hash, tx_height) not in requested_tx:
1050                     self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
1051                     requested_tx.append( (tx_hash, tx_height) )
1052             missing_tx = []
1053
1054             # detect if situation has changed
1055             if not self.interface.is_up_to_date('synchronizer'):
1056                 if self.wallet.is_up_to_date():
1057                     self.wallet.set_up_to_date(False)
1058                     self.was_updated = True
1059             else:
1060                 if not self.wallet.is_up_to_date():
1061                     self.wallet.set_up_to_date(True)
1062                     self.was_updated = True
1063
1064             if self.was_updated:
1065                 self.interface.trigger_callback('updated')
1066                 self.was_updated = False
1067
1068             # 2. get a response
1069             r = self.interface.get_response('synchronizer')
1070
1071             # poke sends None. (needed during stop)
1072             if not r: continue
1073
1074             # 3. handle response
1075             method = r['method']
1076             params = r['params']
1077             result = r.get('result')
1078             error = r.get('error')
1079             if error:
1080                 print "error", r
1081                 continue
1082
1083             if method == 'blockchain.address.subscribe':
1084                 addr = params[0]
1085                 if self.wallet.get_status(self.wallet.get_history(addr)) != result:
1086                     if requested_histories.get(addr) is None:
1087                         self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
1088                         requested_histories[addr] = result
1089
1090             elif method == 'blockchain.address.get_history':
1091                 addr = params[0]
1092                 print_error("receiving history", addr, result)
1093                 if result == ['*']:
1094                     assert requested_histories.pop(addr) == '*'
1095                     self.wallet.receive_history_callback(addr, result)
1096                 else:
1097                     hist = []
1098                     # check that txids are unique
1099                     txids = []
1100                     for item in result:
1101                         tx_hash = item['tx_hash']
1102                         if tx_hash not in txids:
1103                             txids.append(tx_hash)
1104                             hist.append( (tx_hash, item['height']) )
1105
1106                     if len(hist) != len(result):
1107                         raise BaseException("error: server sent history with non-unique txid", result)
1108
1109                     # check that the status corresponds to what was announced
1110                     rs = requested_histories.pop(addr)
1111                     if self.wallet.get_status(hist) != rs:
1112                         raise BaseException("error: status mismatch: %s"%addr)
1113                 
1114                     # store received history
1115                     self.wallet.receive_history_callback(addr, hist)
1116
1117                     # request transactions that we don't have 
1118                     for tx_hash, tx_height in hist:
1119                         if self.wallet.transactions.get(tx_hash) is None:
1120                             if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
1121                                 missing_tx.append( (tx_hash, tx_height) )
1122
1123             elif method == 'blockchain.transaction.get':
1124                 tx_hash = params[0]
1125                 tx_height = params[1]
1126                 assert tx_hash == hash_encode(Hash(result.decode('hex')))
1127                 tx = Transaction(result)
1128                 self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
1129                 self.was_updated = True
1130                 requested_tx.remove( (tx_hash, tx_height) )
1131                 print_error("received tx:", tx_hash, len(tx.raw))
1132
1133             elif method == 'blockchain.transaction.broadcast':
1134                 self.wallet.tx_result = result
1135                 self.wallet.tx_event.set()
1136
1137             else:
1138                 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
1139
1140             if self.was_updated and not requested_tx:
1141                 self.interface.trigger_callback('updated')
1142                 self.was_updated = False
1143
1144