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