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