mktx: send change to one of the accounts involved in the tx
[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 account is None: 
598                     # send change to one of the accounts involved in the tx
599                     address = inputs[0].get('address')
600                     account, _ = self.get_address_index(address)
601
602                 if not self.use_change or account == -1:
603                     change_addr = inputs[-1]['address']
604                 else:
605                     change_addr = self.accounts[account][1][-self.gap_limit_for_change]
606
607             # Insert the change output at a random position in the outputs
608             posn = random.randint(0, len(outputs))
609             outputs[posn:posn] = [( change_addr,  change_amount)]
610         return outputs
611
612
613     def get_history(self, address):
614         with self.lock:
615             return self.history.get(address)
616
617
618     def get_status(self, h):
619         if not h: return None
620         if h == ['*']: return '*'
621         status = ''
622         for tx_hash, height in h:
623             status += tx_hash + ':%d:' % height
624         return hashlib.sha256( status ).digest().encode('hex')
625
626
627     def receive_tx_callback(self, tx_hash, tx, tx_height):
628
629         if not self.check_new_tx(tx_hash, tx):
630             # may happen due to pruning
631             print_error("received transaction that is no longer referenced in history", tx_hash)
632             return
633
634         with self.transaction_lock:
635             self.transactions[tx_hash] = tx
636             if self.verifier and tx_height>0: 
637                 self.verifier.add(tx_hash, tx_height)
638             self.update_tx_outputs(tx_hash)
639
640         self.save()
641
642
643     def receive_history_callback(self, addr, hist):
644
645         if not self.check_new_history(addr, hist):
646             raise BaseException("error: received history for %s is not consistent with known transactions"%addr)
647             
648         with self.lock:
649             self.history[addr] = hist
650             self.save()
651
652         if hist != ['*']:
653             for tx_hash, tx_height in hist:
654                 if tx_height>0:
655                     # add it in case it was previously unconfirmed
656                     if self.verifier: self.verifier.add(tx_hash, tx_height)
657
658
659     def get_tx_history(self, account=None):
660         with self.transaction_lock:
661             history = self.transactions.items()
662             history.sort(key = lambda x: self.verifier.verified_tx.get(x[0]) if self.verifier.verified_tx.get(x[0]) else (1e12,0,0))
663             result = []
664     
665             balance = 0
666             for tx_hash, tx in history:
667                 is_relevant, is_mine, v, fee = self.get_tx_value(tx, account)
668                 if v is not None: balance += v
669
670             c, u = self.get_account_balance(account)
671
672             if balance != c+u:
673                 result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
674
675             balance = c + u - balance
676             for tx_hash, tx in history:
677                 is_relevant, is_mine, value, fee = self.get_tx_value(tx, account)
678                 if not is_relevant:
679                     continue
680                 if value is not None:
681                     balance += value
682
683                 conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
684                 result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
685
686         return result
687
688
689     def get_label(self, tx_hash):
690         label = self.labels.get(tx_hash)
691         is_default = (label == '') or (label is None)
692         if is_default: label = self.get_default_label(tx_hash)
693         return label, is_default
694
695
696     def get_default_label(self, tx_hash):
697         tx = self.transactions.get(tx_hash)
698         default_label = ''
699         if tx:
700             is_relevant, is_mine, _, _ = self.get_tx_value(tx)
701             if is_mine:
702                 for o in tx.outputs:
703                     o_addr, _ = o
704                     if not self.is_mine(o_addr):
705                         try:
706                             default_label = self.labels[o_addr]
707                         except KeyError:
708                             default_label = o_addr
709                         break
710                 else:
711                     default_label = '(internal)'
712             else:
713                 for o in tx.outputs:
714                     o_addr, _ = o
715                     if self.is_mine(o_addr) and not self.is_change(o_addr):
716                         break
717                 else:
718                     for o in tx.outputs:
719                         o_addr, _ = o
720                         if self.is_mine(o_addr):
721                             break
722                     else:
723                         o_addr = None
724
725                 if o_addr:
726                     dest_label = self.labels.get(o_addr)
727                     try:
728                         default_label = self.labels[o_addr]
729                     except KeyError:
730                         default_label = o_addr
731
732         return default_label
733
734
735     def mktx(self, outputs, password, fee=None, change_addr=None, account=None ):
736         """
737         create a transaction
738         account parameter:
739            None means use all accounts
740            -1 means imported keys
741            0, 1, etc are seed accounts
742         """
743         
744         for address, x in outputs:
745             assert is_valid(address)
746
747         amount = sum( map(lambda x:x[1], outputs) )
748
749         inputs, total, fee = self.choose_tx_inputs( amount, fee, account )
750         if not inputs:
751             raise ValueError("Not enough funds")
752
753         outputs = self.add_tx_change(inputs, outputs, amount, fee, total, change_addr, account)
754
755         tx = Transaction.from_io(inputs, outputs)
756
757         pk_addresses = []
758         for i in range(len(tx.inputs)):
759             txin = tx.inputs[i]
760             address = txin['address']
761             if address in self.imported_keys.keys():
762                 pk_addresses.append(address)
763                 continue
764             account, sequence = self.get_address_index(address)
765             txin['KeyID'] = (account, 'Electrum', sequence) # used by the server to find the key
766             pk_addr, redeemScript = self.sequences[account].get_input_info(sequence)
767             if redeemScript: txin['redeemScript'] = redeemScript
768             pk_addresses.append(pk_addr)
769
770         # get all private keys at once.
771         if self.seed:
772             private_keys = self.get_private_keys(pk_addresses, password)
773             tx.sign(private_keys)
774
775         for address, x in outputs:
776             if address not in self.addressbook and not self.is_mine(address):
777                 self.addressbook.append(address)
778
779         return tx
780
781
782
783     def sendtx(self, tx):
784         # synchronous
785         h = self.send_tx(tx)
786         self.tx_event.wait()
787         return self.receive_tx(h)
788
789     def send_tx(self, tx):
790         # asynchronous
791         self.tx_event.clear()
792         self.interface.send([('blockchain.transaction.broadcast', [str(tx)])], 'synchronizer')
793         return tx.hash()
794
795     def receive_tx(self,tx_hash):
796         out = self.tx_result 
797         if out != tx_hash:
798             return False, "error: " + out
799         return True, out
800
801
802
803     def update_password(self, seed, old_password, new_password):
804         if new_password == '': new_password = None
805         self.use_encryption = (new_password != None)
806         self.seed = pw_encode( seed, new_password)
807         self.config.set_key('seed', self.seed, True)
808         for k in self.imported_keys.keys():
809             a = self.imported_keys[k]
810             b = pw_decode(a, old_password)
811             c = pw_encode(b, new_password)
812             self.imported_keys[k] = c
813         self.save()
814
815
816
817     def freeze(self,addr):
818         if self.is_mine(addr) and addr not in self.frozen_addresses:
819             self.unprioritize(addr)
820             self.frozen_addresses.append(addr)
821             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
822             return True
823         else:
824             return False
825
826     def unfreeze(self,addr):
827         if self.is_mine(addr) and addr in self.frozen_addresses:
828             self.frozen_addresses.remove(addr)
829             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
830             return True
831         else:
832             return False
833
834     def prioritize(self,addr):
835         if self.is_mine(addr) and addr not in self.prioritized_addresses:
836             self.unfreeze(addr)
837             self.prioritized_addresses.append(addr)
838             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
839             return True
840         else:
841             return False
842
843     def unprioritize(self,addr):
844         if self.is_mine(addr) and addr in self.prioritized_addresses:
845             self.prioritized_addresses.remove(addr)
846             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
847             return True
848         else:
849             return False
850
851     def save(self):
852         tx = {}
853         for k,v in self.transactions.items():
854             tx[k] = str(v)
855             
856         s = {
857             'use_encryption': self.use_encryption,
858             'use_change': self.use_change,
859             'fee_per_kb': self.fee,
860             'accounts': self.accounts,
861             'addr_history': self.history, 
862             'labels': self.labels,
863             'contacts': self.addressbook,
864             'imported_keys': self.imported_keys,
865             'num_zeros': self.num_zeros,
866             'frozen_addresses': self.frozen_addresses,
867             'prioritized_addresses': self.prioritized_addresses,
868             'gap_limit': self.gap_limit,
869             'transactions': tx,
870         }
871         for k, v in s.items():
872             self.config.set_key(k,v)
873         self.config.save()
874
875     def set_verifier(self, verifier):
876         self.verifier = verifier
877
878         # review transactions that are in the history
879         for addr, hist in self.history.items():
880             if hist == ['*']: continue
881             for tx_hash, tx_height in hist:
882                 if tx_height>0:
883                     # add it in case it was previously unconfirmed
884                     self.verifier.add(tx_hash, tx_height)
885
886
887         # if we are on a pruning server, remove unverified transactions
888         vr = self.verifier.transactions.keys() + self.verifier.verified_tx.keys()
889         for tx_hash in self.transactions.keys():
890             if tx_hash not in vr:
891                 self.transactions.pop(tx_hash)
892
893
894
895     def check_new_history(self, addr, hist):
896         
897         # check that all tx in hist are relevant
898         if hist != ['*']:
899             for tx_hash, height in hist:
900                 tx = self.transactions.get(tx_hash)
901                 if not tx: continue
902                 if not tx.has_address(addr):
903                     return False
904
905         # check that we are not "orphaning" a transaction
906         old_hist = self.history.get(addr,[])
907         if old_hist == ['*']: return True
908
909         for tx_hash, height in old_hist:
910             if tx_hash in map(lambda x:x[0], hist): continue
911             found = False
912             for _addr, _hist in self.history.items():
913                 if _addr == addr: continue
914                 if _hist == ['*']: continue
915                 _tx_hist = map(lambda x:x[0], _hist)
916                 if tx_hash in _tx_hist:
917                     found = True
918                     break
919
920             if not found:
921                 tx = self.transactions.get(tx_hash)
922                 # tx might not be there
923                 if not tx: continue
924                 
925                 # already verified?
926                 if self.verifier.get_height(tx_hash):
927                     continue
928                 # unconfirmed tx
929                 print_error("new history is orphaning transaction:", tx_hash)
930                 # check that all outputs are not mine, request histories
931                 ext_requests = []
932                 for _addr, _v in tx.outputs:
933                     # assert not self.is_mine(_addr)
934                     ext_requests.append( ('blockchain.address.get_history', [_addr]) )
935
936                 ext_h = self.interface.synchronous_get(ext_requests)
937                 print_error("sync:", ext_requests, ext_h)
938                 height = None
939                 for h in ext_h:
940                     if h == ['*']: continue
941                     for item in h:
942                         if item.get('tx_hash') == tx_hash:
943                             height = item.get('height')
944                 if height:
945                     print_error("found height for", tx_hash, height)
946                     self.verifier.add(tx_hash, height)
947                 else:
948                     print_error("removing orphaned tx from history", tx_hash)
949                     self.transactions.pop(tx_hash)
950
951         return True
952
953
954
955     def check_new_tx(self, tx_hash, tx):
956         # 1 check that tx is referenced in addr_history. 
957         addresses = []
958         for addr, hist in self.history.items():
959             if hist == ['*']:continue
960             for txh, height in hist:
961                 if txh == tx_hash: 
962                     addresses.append(addr)
963
964         if not addresses:
965             return False
966
967         # 2 check that referencing addresses are in the tx
968         for addr in addresses:
969             if not tx.has_address(addr):
970                 return False
971
972         return True
973
974
975
976
977 class WalletSynchronizer(threading.Thread):
978
979
980     def __init__(self, wallet, config):
981         threading.Thread.__init__(self)
982         self.daemon = True
983         self.wallet = wallet
984         self.interface = self.wallet.interface
985         self.interface.register_channel('synchronizer')
986         self.wallet.interface.register_callback('connected', lambda: self.wallet.set_up_to_date(False))
987         self.was_updated = True
988         self.running = False
989         self.lock = threading.Lock()
990
991     def stop(self):
992         with self.lock: self.running = False
993         self.interface.poke('synchronizer')
994
995     def is_running(self):
996         with self.lock: return self.running
997
998     
999     def subscribe_to_addresses(self, addresses):
1000         messages = []
1001         for addr in addresses:
1002             messages.append(('blockchain.address.subscribe', [addr]))
1003         self.interface.send( messages, 'synchronizer')
1004
1005
1006     def run(self):
1007         with self.lock: self.running = True
1008
1009         requested_tx = []
1010         missing_tx = []
1011         requested_histories = {}
1012
1013         # request any missing transactions
1014         for history in self.wallet.history.values():
1015             if history == ['*']: continue
1016             for tx_hash, tx_height in history:
1017                 if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
1018                     missing_tx.append( (tx_hash, tx_height) )
1019         print_error("missing tx", missing_tx)
1020
1021         # wait until we are connected, in case the user is not connected
1022         while not self.interface.is_connected:
1023             time.sleep(1)
1024         
1025         # subscriptions
1026         self.subscribe_to_addresses(self.wallet.addresses(True))
1027
1028         while self.is_running():
1029             # 1. create new addresses
1030             new_addresses = self.wallet.synchronize()
1031
1032             # request missing addresses
1033             if new_addresses:
1034                 self.subscribe_to_addresses(new_addresses)
1035
1036             # request missing transactions
1037             for tx_hash, tx_height in missing_tx:
1038                 if (tx_hash, tx_height) not in requested_tx:
1039                     self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
1040                     requested_tx.append( (tx_hash, tx_height) )
1041             missing_tx = []
1042
1043             # detect if situation has changed
1044             if not self.interface.is_up_to_date('synchronizer'):
1045                 if self.wallet.is_up_to_date():
1046                     self.wallet.set_up_to_date(False)
1047                     self.was_updated = True
1048             else:
1049                 if not self.wallet.is_up_to_date():
1050                     self.wallet.set_up_to_date(True)
1051                     self.was_updated = True
1052
1053             if self.was_updated:
1054                 self.interface.trigger_callback('updated')
1055                 self.was_updated = False
1056
1057             # 2. get a response
1058             r = self.interface.get_response('synchronizer')
1059
1060             # poke sends None. (needed during stop)
1061             if not r: continue
1062
1063             # 3. handle response
1064             method = r['method']
1065             params = r['params']
1066             result = r.get('result')
1067             error = r.get('error')
1068             if error:
1069                 print "error", r
1070                 continue
1071
1072             if method == 'blockchain.address.subscribe':
1073                 addr = params[0]
1074                 if self.wallet.get_status(self.wallet.get_history(addr)) != result:
1075                     if requested_histories.get(addr) is None:
1076                         self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
1077                         requested_histories[addr] = result
1078
1079             elif method == 'blockchain.address.get_history':
1080                 addr = params[0]
1081                 print_error("receiving history", addr, result)
1082                 if result == ['*']:
1083                     assert requested_histories.pop(addr) == '*'
1084                     self.wallet.receive_history_callback(addr, result)
1085                 else:
1086                     hist = []
1087                     # check that txids are unique
1088                     txids = []
1089                     for item in result:
1090                         tx_hash = item['tx_hash']
1091                         if tx_hash not in txids:
1092                             txids.append(tx_hash)
1093                             hist.append( (tx_hash, item['height']) )
1094
1095                     if len(hist) != len(result):
1096                         raise BaseException("error: server sent history with non-unique txid", result)
1097
1098                     # check that the status corresponds to what was announced
1099                     rs = requested_histories.pop(addr)
1100                     if self.wallet.get_status(hist) != rs:
1101                         raise BaseException("error: status mismatch: %s"%addr)
1102                 
1103                     # store received history
1104                     self.wallet.receive_history_callback(addr, hist)
1105
1106                     # request transactions that we don't have 
1107                     for tx_hash, tx_height in hist:
1108                         if self.wallet.transactions.get(tx_hash) is None:
1109                             if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
1110                                 missing_tx.append( (tx_hash, tx_height) )
1111
1112             elif method == 'blockchain.transaction.get':
1113                 tx_hash = params[0]
1114                 tx_height = params[1]
1115                 assert tx_hash == hash_encode(Hash(result.decode('hex')))
1116                 tx = Transaction(result)
1117                 self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
1118                 self.was_updated = True
1119                 requested_tx.remove( (tx_hash, tx_height) )
1120                 print_error("received tx:", tx_hash, len(tx.raw))
1121
1122             elif method == 'blockchain.transaction.broadcast':
1123                 self.wallet.tx_result = result
1124                 self.wallet.tx_event.set()
1125
1126             else:
1127                 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
1128
1129             if self.was_updated and not requested_tx:
1130                 self.interface.trigger_callback('updated')
1131                 self.was_updated = False
1132
1133