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