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