handle multiple accounts with separation
[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         return address
148         
149
150     def init_seed(self, seed):
151         if self.seed: raise BaseException("a seed exists")
152         if not seed: 
153             seed = random_seed(128)
154         self.seed = seed 
155         self.config.set_key('seed', self.seed, True)
156         self.config.set_key('seed_version', self.seed_version, True)
157         mpk = self.SequenceClass.mpk_from_seed(self.seed)
158         self.init_sequence(mpk)
159
160
161     def init_sequence(self, mpk):
162         self.config.set_key('master_public_key', mpk, True)
163         self.sequences[0] = self.SequenceClass(mpk)
164         self.accounts[0] = { 0:[], 1:[], 'name':'Main account' }
165         self.config.set_key('accounts', self.accounts, True)
166
167
168     def addresses(self, include_change = True):
169         o = self.get_account_addresses(-1, include_change)
170         for a in self.accounts.keys():
171             o += self.get_account_addresses(a, include_change)
172         return o
173
174
175     def is_mine(self, address):
176         return address in self.addresses(True)
177
178     def is_change(self, address):
179         if not self.is_mine(address): return False
180         if address in self.imported_keys.keys(): return False
181         acct, s = self.get_address_index(address)
182         return s[0] == 1
183
184     def get_master_public_key(self):
185         return self.config.get("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_relevant, 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     def get_num_tx(self, address):
411         n = 0 
412         for tx in self.transactions.values():
413             if address in map(lambda x:x[0], tx.outputs): n += 1
414         return n
415
416
417     def get_address_flags(self, addr):
418         flags = "C" if self.is_change(addr) else "I" if addr in self.imported_keys.keys() else "-" 
419         flags += "F" if addr in self.frozen_addresses else "P" if addr in self.prioritized_addresses else "-"
420         return flags
421         
422
423     def get_tx_value(self, tx, account=None):
424         domain = self.get_account_addresses(account)
425         return tx.get_value(domain, self.prevout_values)
426
427     
428     def update_tx_outputs(self, tx_hash):
429         tx = self.transactions.get(tx_hash)
430
431         for i, (addr, value) in enumerate(tx.outputs):
432             key = tx_hash+ ':%d'%i
433             self.prevout_values[key] = value
434
435         for item in tx.inputs:
436             if self.is_mine(item.get('address')):
437                 key = item['prevout_hash'] + ':%d'%item['prevout_n']
438                 self.spent_outputs.append(key)
439
440
441     def get_addr_balance(self, address):
442         assert self.is_mine(address)
443         h = self.history.get(address,[])
444         if h == ['*']: return 0,0
445         c = u = 0
446         received_coins = []   # list of coins received at address
447
448         for tx_hash, tx_height in h:
449             tx = self.transactions.get(tx_hash)
450             if not tx: continue
451
452             for i, (addr, value) in enumerate(tx.outputs):
453                 if addr == address:
454                     key = tx_hash + ':%d'%i
455                     received_coins.append(key)
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             for i, (addr, value) in enumerate(tx.outputs):
471                 key = tx_hash + ':%d'%i
472                 if addr == address:
473                     v += value
474
475             if tx_height:
476                 c += v
477             else:
478                 u += v
479         return c, u
480
481
482     def get_accounts(self):
483         accounts = {}
484         for k, account in self.accounts.items():
485             accounts[k] = account.get('name')
486         if self.imported_keys:
487             accounts[-1] = 'Imported keys'
488         return accounts
489
490     def get_account_addresses(self, a, include_change=True):
491         if a is None:
492             o = self.addresses(True)
493         elif a == -1:
494             o = self.imported_keys.keys()
495         else:
496             ac = self.accounts[a]
497             o = ac[0][:]
498             if include_change: o += ac[1]
499         return o
500
501     def get_imported_balance(self):
502         cc = uu = 0
503         for addr in self.imported_keys.keys():
504             c, u = self.get_addr_balance(addr)
505             cc += c
506             uu += u
507         return cc, uu
508
509     def get_account_balance(self, account):
510         if account is None:
511             return self.get_balance()
512         elif account == -1:
513             return self.get_imported_balance()
514         
515         conf = unconf = 0
516         for addr in self.get_account_addresses(account): 
517             c, u = self.get_addr_balance(addr)
518             conf += c
519             unconf += u
520         return conf, unconf
521
522     def get_balance(self):
523         cc = uu = 0
524         for a in self.accounts.keys():
525             c, u = self.get_account_balance(a)
526             cc += c
527             uu += u
528         c, u = self.get_imported_balance()
529         cc += c
530         uu += u
531         return cc, uu
532
533
534     def get_unspent_coins(self, domain=None):
535         coins = []
536         if domain is None: domain = self.addresses(True)
537         for addr in domain:
538             h = self.history.get(addr, [])
539             if h == ['*']: continue
540             for tx_hash, tx_height in h:
541                 tx = self.transactions.get(tx_hash)
542                 if tx is None: raise BaseException("Wallet not synchronized")
543                 for output in tx.d.get('outputs'):
544                     if output.get('address') != addr: continue
545                     key = tx_hash + ":%d" % output.get('index')
546                     if key in self.spent_outputs: continue
547                     output['tx_hash'] = tx_hash
548                     coins.append(output)
549         return coins
550
551
552
553     def choose_tx_inputs( self, amount, fixed_fee, account = None ):
554         """ todo: minimize tx size """
555         total = 0
556         fee = self.fee if fixed_fee is None else fixed_fee
557         domain = self.get_account_addresses(account)
558         coins = []
559         prioritized_coins = []
560         for i in self.frozen_addresses:
561             if i in domain: domain.remove(i)
562
563         for i in self.prioritized_addresses:
564             if i in domain: domain.remove(i)
565
566         coins = self.get_unspent_coins(domain)
567         prioritized_coins = self.get_unspent_coins(self.prioritized_addresses)
568
569         inputs = []
570         coins = prioritized_coins + coins
571
572         for item in coins: 
573             addr = item.get('address')
574             v = item.get('value')
575             total += v
576             inputs.append( item )
577             fee = self.estimated_fee(inputs) if fixed_fee is None else fixed_fee
578             if total >= amount + fee: break
579         else:
580             inputs = []
581
582         return inputs, total, fee
583
584
585     def estimated_fee(self, inputs):
586         estimated_size =  len(inputs) * 180 + 80     # this assumes non-compressed keys
587         fee = self.fee * int(round(estimated_size/1024.))
588         if fee == 0: fee = self.fee
589         return fee
590
591
592     def add_tx_change( self, inputs, outputs, amount, fee, total, change_addr=None, account=0 ):
593         "add change to a transaction"
594         change_amount = total - ( amount + fee )
595         if change_amount != 0:
596             if not change_addr:
597                 if not self.use_change or account == -1:
598                     change_addr = inputs[-1]['address']
599                 else:
600                     if account is None: account = 0
601                     change_addr = self.accounts[account][1][-self.gap_limit_for_change]
602
603             # Insert the change output at a random position in the outputs
604             posn = random.randint(0, len(outputs))
605             outputs[posn:posn] = [( change_addr,  change_amount)]
606         return outputs
607
608
609     def get_history(self, address):
610         with self.lock:
611             return self.history.get(address)
612
613
614     def get_status(self, h):
615         if not h: return None
616         if h == ['*']: return '*'
617         status = ''
618         for tx_hash, height in h:
619             status += tx_hash + ':%d:' % height
620         return hashlib.sha256( status ).digest().encode('hex')
621
622
623     def receive_tx_callback(self, tx_hash, tx, tx_height):
624
625         if not self.check_new_tx(tx_hash, tx):
626             # may happen due to pruning
627             print_error("received transaction that is no longer referenced in history", tx_hash)
628             return
629
630         with self.transaction_lock:
631             self.transactions[tx_hash] = tx
632             if self.verifier and tx_height>0: 
633                 self.verifier.add(tx_hash, tx_height)
634             self.update_tx_outputs(tx_hash)
635
636         self.save()
637
638
639     def receive_history_callback(self, addr, hist):
640
641         if not self.check_new_history(addr, hist):
642             raise BaseException("error: received history for %s is not consistent with known transactions"%addr)
643             
644         with self.lock:
645             self.history[addr] = hist
646             self.save()
647
648         if hist != ['*']:
649             for tx_hash, tx_height in hist:
650                 if tx_height>0:
651                     # add it in case it was previously unconfirmed
652                     if self.verifier: self.verifier.add(tx_hash, tx_height)
653
654
655     def get_tx_history(self, account=None):
656         with self.transaction_lock:
657             history = self.transactions.items()
658             history.sort(key = lambda x: self.verifier.verified_tx.get(x[0]) if self.verifier.verified_tx.get(x[0]) else (1e12,0,0))
659             result = []
660     
661             balance = 0
662             for tx_hash, tx in history:
663                 is_relevant, is_mine, v, fee = self.get_tx_value(tx, account)
664                 if v is not None: balance += v
665
666             c, u = self.get_account_balance(account)
667
668             if balance != c+u:
669                 result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
670
671             balance = c + u - balance
672             for tx_hash, tx in history:
673                 is_relevant, is_mine, value, fee = self.get_tx_value(tx, account)
674                 if not is_relevant:
675                     continue
676                 if value is not None:
677                     balance += value
678
679                 conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
680                 result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
681
682         return result
683
684
685     def get_label(self, tx_hash):
686         label = self.labels.get(tx_hash)
687         is_default = (label == '') or (label is None)
688         if is_default: label = self.get_default_label(tx_hash)
689         return label, is_default
690
691
692     def get_default_label(self, tx_hash):
693         tx = self.transactions.get(tx_hash)
694         default_label = ''
695         if tx:
696             is_relevant, is_mine, _, _ = self.get_tx_value(tx)
697             if is_mine:
698                 for o in tx.outputs:
699                     o_addr, _ = o
700                     if not self.is_mine(o_addr):
701                         try:
702                             default_label = self.labels[o_addr]
703                         except KeyError:
704                             default_label = o_addr
705                         break
706                 else:
707                     default_label = '(internal)'
708             else:
709                 for o in tx.outputs:
710                     o_addr, _ = o
711                     if self.is_mine(o_addr) and not self.is_change(o_addr):
712                         break
713                 else:
714                     for o in tx.outputs:
715                         o_addr, _ = o
716                         if self.is_mine(o_addr):
717                             break
718                     else:
719                         o_addr = None
720
721                 if o_addr:
722                     dest_label = self.labels.get(o_addr)
723                     try:
724                         default_label = self.labels[o_addr]
725                     except KeyError:
726                         default_label = o_addr
727
728         return default_label
729
730
731     def mktx(self, outputs, password, fee=None, change_addr=None, account=None ):
732         """
733         create a transaction
734         account parameter:
735            None means use all accounts
736            -1 means imported keys
737            0, 1, etc are seed accounts
738         """
739         
740         for address, x in outputs:
741             assert is_valid(address)
742
743         amount = sum( map(lambda x:x[1], outputs) )
744         
745         domain = self.get_account_addresses(account)
746             
747         inputs, total, fee = self.choose_tx_inputs( amount, fee, domain )
748         if not inputs:
749             raise ValueError("Not enough funds")
750
751         outputs = self.add_tx_change(inputs, outputs, amount, fee, total, change_addr, account)
752
753         tx = Transaction.from_io(inputs, outputs)
754
755         pk_addresses = []
756         for i in range(len(tx.inputs)):
757             txin = tx.inputs[i]
758             address = txin['address']
759             if address in self.imported_keys.keys():
760                 pk_addresses.append(address)
761                 continue
762             account, sequence = self.get_address_index(address)
763             txin['KeyID'] = (account, 'Electrum', sequence) # used by the server to find the key
764             pk_addr, redeemScript = self.sequences[account].get_input_info(sequence)
765             if redeemScript: txin['redeemScript'] = redeemScript
766             pk_addresses.append(pk_addr)
767
768         # get all private keys at once.
769         if self.seed:
770             private_keys = self.get_private_keys(pk_addresses, password)
771             tx.sign(private_keys)
772
773         for address, x in outputs:
774             if address not in self.addressbook and not self.is_mine(address):
775                 self.addressbook.append(address)
776
777         return tx
778
779
780
781     def sendtx(self, tx):
782         # synchronous
783         h = self.send_tx(tx)
784         self.tx_event.wait()
785         return self.receive_tx(h)
786
787     def send_tx(self, tx):
788         # asynchronous
789         self.tx_event.clear()
790         self.interface.send([('blockchain.transaction.broadcast', [str(tx)])], 'synchronizer')
791         return tx.hash()
792
793     def receive_tx(self,tx_hash):
794         out = self.tx_result 
795         if out != tx_hash:
796             return False, "error: " + out
797         return True, out
798
799
800
801     def update_password(self, seed, old_password, new_password):
802         if new_password == '': new_password = None
803         self.use_encryption = (new_password != None)
804         self.seed = pw_encode( seed, new_password)
805         self.config.set_key('seed', self.seed, True)
806         for k in self.imported_keys.keys():
807             a = self.imported_keys[k]
808             b = pw_decode(a, old_password)
809             c = pw_encode(b, new_password)
810             self.imported_keys[k] = c
811         self.save()
812
813
814
815     def freeze(self,addr):
816         if self.is_mine(addr) and addr not in self.frozen_addresses:
817             self.unprioritize(addr)
818             self.frozen_addresses.append(addr)
819             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
820             return True
821         else:
822             return False
823
824     def unfreeze(self,addr):
825         if self.is_mine(addr) and addr in self.frozen_addresses:
826             self.frozen_addresses.remove(addr)
827             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
828             return True
829         else:
830             return False
831
832     def prioritize(self,addr):
833         if self.is_mine(addr) and addr not in self.prioritized_addresses:
834             self.unfreeze(addr)
835             self.prioritized_addresses.append(addr)
836             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
837             return True
838         else:
839             return False
840
841     def unprioritize(self,addr):
842         if self.is_mine(addr) and addr in self.prioritized_addresses:
843             self.prioritized_addresses.remove(addr)
844             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
845             return True
846         else:
847             return False
848
849     def save(self):
850         tx = {}
851         for k,v in self.transactions.items():
852             tx[k] = str(v)
853             
854         s = {
855             'use_encryption': self.use_encryption,
856             'use_change': self.use_change,
857             'fee_per_kb': self.fee,
858             'accounts': self.accounts,
859             'addr_history': self.history, 
860             'labels': self.labels,
861             'contacts': self.addressbook,
862             'imported_keys': self.imported_keys,
863             'num_zeros': self.num_zeros,
864             'frozen_addresses': self.frozen_addresses,
865             'prioritized_addresses': self.prioritized_addresses,
866             'gap_limit': self.gap_limit,
867             'transactions': tx,
868         }
869         for k, v in s.items():
870             self.config.set_key(k,v)
871         self.config.save()
872
873     def set_verifier(self, verifier):
874         self.verifier = verifier
875
876         # review transactions that are in the history
877         for addr, hist in self.history.items():
878             if hist == ['*']: continue
879             for tx_hash, tx_height in hist:
880                 if tx_height>0:
881                     # add it in case it was previously unconfirmed
882                     self.verifier.add(tx_hash, tx_height)
883
884
885         # if we are on a pruning server, remove unverified transactions
886         vr = self.verifier.transactions.keys() + self.verifier.verified_tx.keys()
887         for tx_hash in self.transactions.keys():
888             if tx_hash not in vr:
889                 self.transactions.pop(tx_hash)
890
891
892
893     def check_new_history(self, addr, hist):
894         
895         # check that all tx in hist are relevant
896         if hist != ['*']:
897             for tx_hash, height in hist:
898                 tx = self.transactions.get(tx_hash)
899                 if not tx: continue
900                 if not tx.has_address(addr):
901                     return False
902
903         # check that we are not "orphaning" a transaction
904         old_hist = self.history.get(addr,[])
905         if old_hist == ['*']: return True
906
907         for tx_hash, height in old_hist:
908             if tx_hash in map(lambda x:x[0], hist): continue
909             found = False
910             for _addr, _hist in self.history.items():
911                 if _addr == addr: continue
912                 if _hist == ['*']: continue
913                 _tx_hist = map(lambda x:x[0], _hist)
914                 if tx_hash in _tx_hist:
915                     found = True
916                     break
917
918             if not found:
919                 tx = self.transactions.get(tx_hash)
920                 # tx might not be there
921                 if not tx: continue
922                 
923                 # already verified?
924                 if self.verifier.get_height(tx_hash):
925                     continue
926                 # unconfirmed tx
927                 print_error("new history is orphaning transaction:", tx_hash)
928                 # check that all outputs are not mine, request histories
929                 ext_requests = []
930                 for _addr, _v in tx.outputs:
931                     # assert not self.is_mine(_addr)
932                     ext_requests.append( ('blockchain.address.get_history', [_addr]) )
933
934                 ext_h = self.interface.synchronous_get(ext_requests)
935                 print_error("sync:", ext_requests, ext_h)
936                 height = None
937                 for h in ext_h:
938                     if h == ['*']: continue
939                     for item in h:
940                         if item.get('tx_hash') == tx_hash:
941                             height = item.get('height')
942                 if height:
943                     print_error("found height for", tx_hash, height)
944                     self.verifier.add(tx_hash, height)
945                 else:
946                     print_error("removing orphaned tx from history", tx_hash)
947                     self.transactions.pop(tx_hash)
948
949         return True
950
951
952
953     def check_new_tx(self, tx_hash, tx):
954         # 1 check that tx is referenced in addr_history. 
955         addresses = []
956         for addr, hist in self.history.items():
957             if hist == ['*']:continue
958             for txh, height in hist:
959                 if txh == tx_hash: 
960                     addresses.append(addr)
961
962         if not addresses:
963             return False
964
965         # 2 check that referencing addresses are in the tx
966         for addr in addresses:
967             if not tx.has_address(addr):
968                 return False
969
970         return True
971
972
973
974
975 class WalletSynchronizer(threading.Thread):
976
977
978     def __init__(self, wallet, config):
979         threading.Thread.__init__(self)
980         self.daemon = True
981         self.wallet = wallet
982         self.interface = self.wallet.interface
983         self.interface.register_channel('synchronizer')
984         self.wallet.interface.register_callback('connected', lambda: self.wallet.set_up_to_date(False))
985         self.was_updated = True
986         self.running = False
987         self.lock = threading.Lock()
988
989     def stop(self):
990         with self.lock: self.running = False
991         self.interface.poke('synchronizer')
992
993     def is_running(self):
994         with self.lock: return self.running
995
996     
997     def subscribe_to_addresses(self, addresses):
998         messages = []
999         for addr in addresses:
1000             messages.append(('blockchain.address.subscribe', [addr]))
1001         self.interface.send( messages, 'synchronizer')
1002
1003
1004     def run(self):
1005         with self.lock: self.running = True
1006
1007         requested_tx = []
1008         missing_tx = []
1009         requested_histories = {}
1010
1011         # request any missing transactions
1012         for history in self.wallet.history.values():
1013             if history == ['*']: continue
1014             for tx_hash, tx_height in history:
1015                 if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
1016                     missing_tx.append( (tx_hash, tx_height) )
1017         print_error("missing tx", missing_tx)
1018
1019         # wait until we are connected, in case the user is not connected
1020         while not self.interface.is_connected:
1021             time.sleep(1)
1022         
1023         # subscriptions
1024         self.subscribe_to_addresses(self.wallet.addresses(True))
1025
1026         while self.is_running():
1027             # 1. create new addresses
1028             new_addresses = self.wallet.synchronize()
1029
1030             # request missing addresses
1031             if new_addresses:
1032                 self.subscribe_to_addresses(new_addresses)
1033
1034             # request missing transactions
1035             for tx_hash, tx_height in missing_tx:
1036                 if (tx_hash, tx_height) not in requested_tx:
1037                     self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
1038                     requested_tx.append( (tx_hash, tx_height) )
1039             missing_tx = []
1040
1041             # detect if situation has changed
1042             if not self.interface.is_up_to_date('synchronizer'):
1043                 if self.wallet.is_up_to_date():
1044                     self.wallet.set_up_to_date(False)
1045                     self.was_updated = True
1046             else:
1047                 if not self.wallet.is_up_to_date():
1048                     self.wallet.set_up_to_date(True)
1049                     self.was_updated = True
1050
1051             if self.was_updated:
1052                 self.interface.trigger_callback('updated')
1053                 self.was_updated = False
1054
1055             # 2. get a response
1056             r = self.interface.get_response('synchronizer')
1057
1058             # poke sends None. (needed during stop)
1059             if not r: continue
1060
1061             # 3. handle response
1062             method = r['method']
1063             params = r['params']
1064             result = r.get('result')
1065             error = r.get('error')
1066             if error:
1067                 print "error", r
1068                 continue
1069
1070             if method == 'blockchain.address.subscribe':
1071                 addr = params[0]
1072                 if self.wallet.get_status(self.wallet.get_history(addr)) != result:
1073                     if requested_histories.get(addr) is None:
1074                         self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
1075                         requested_histories[addr] = result
1076
1077             elif method == 'blockchain.address.get_history':
1078                 addr = params[0]
1079                 print_error("receiving history", addr, result)
1080                 if result == ['*']:
1081                     assert requested_histories.pop(addr) == '*'
1082                     self.wallet.receive_history_callback(addr, result)
1083                 else:
1084                     hist = []
1085                     # check that txids are unique
1086                     txids = []
1087                     for item in result:
1088                         tx_hash = item['tx_hash']
1089                         if tx_hash not in txids:
1090                             txids.append(tx_hash)
1091                             hist.append( (tx_hash, item['height']) )
1092
1093                     if len(hist) != len(result):
1094                         raise BaseException("error: server sent history with non-unique txid", result)
1095
1096                     # check that the status corresponds to what was announced
1097                     rs = requested_histories.pop(addr)
1098                     if self.wallet.get_status(hist) != rs:
1099                         raise BaseException("error: status mismatch: %s"%addr)
1100                 
1101                     # store received history
1102                     self.wallet.receive_history_callback(addr, hist)
1103
1104                     # request transactions that we don't have 
1105                     for tx_hash, tx_height in hist:
1106                         if self.wallet.transactions.get(tx_hash) is None:
1107                             if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
1108                                 missing_tx.append( (tx_hash, tx_height) )
1109
1110             elif method == 'blockchain.transaction.get':
1111                 tx_hash = params[0]
1112                 tx_height = params[1]
1113                 assert tx_hash == hash_encode(Hash(result.decode('hex')))
1114                 tx = Transaction(result)
1115                 self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
1116                 self.was_updated = True
1117                 requested_tx.remove( (tx_hash, tx_height) )
1118                 print_error("received tx:", tx_hash, len(tx.raw))
1119
1120             elif method == 'blockchain.transaction.broadcast':
1121                 self.wallet.tx_result = result
1122                 self.wallet.tx_event.set()
1123
1124             else:
1125                 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
1126
1127             if self.was_updated and not requested_tx:
1128                 self.interface.trigger_callback('updated')
1129                 self.was_updated = False
1130
1131