wallet set_fee 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 set_fee(self, fee):
877         if self.fee != fee:
878             self.fee = fee
879             self.config.set_key('fee_per_kb', self.fee, True)
880         
881
882     def save(self):
883         tx = {}
884         for k,v in self.transactions.items():
885             tx[k] = str(v)
886             
887         s = {
888             'use_change': self.use_change,
889             'fee_per_kb': self.fee,
890             'accounts': self.accounts,
891             'addr_history': self.history, 
892             'labels': self.labels,
893             'contacts': self.addressbook,
894             'num_zeros': self.num_zeros,
895             'frozen_addresses': self.frozen_addresses,
896             'prioritized_addresses': self.prioritized_addresses,
897             'gap_limit': self.gap_limit,
898             'transactions': tx,
899         }
900         for k, v in s.items():
901             self.config.set_key(k,v)
902         self.config.save()
903
904     def set_verifier(self, verifier):
905         self.verifier = verifier
906
907         # review transactions that are in the history
908         for addr, hist in self.history.items():
909             if hist == ['*']: continue
910             for tx_hash, tx_height in hist:
911                 if tx_height>0:
912                     # add it in case it was previously unconfirmed
913                     self.verifier.add(tx_hash, tx_height)
914
915
916         # if we are on a pruning server, remove unverified transactions
917         vr = self.verifier.transactions.keys() + self.verifier.verified_tx.keys()
918         for tx_hash in self.transactions.keys():
919             if tx_hash not in vr:
920                 self.transactions.pop(tx_hash)
921
922
923
924     def check_new_history(self, addr, hist):
925         
926         # check that all tx in hist are relevant
927         if hist != ['*']:
928             for tx_hash, height in hist:
929                 tx = self.transactions.get(tx_hash)
930                 if not tx: continue
931                 if not tx.has_address(addr):
932                     return False
933
934         # check that we are not "orphaning" a transaction
935         old_hist = self.history.get(addr,[])
936         if old_hist == ['*']: return True
937
938         for tx_hash, height in old_hist:
939             if tx_hash in map(lambda x:x[0], hist): continue
940             found = False
941             for _addr, _hist in self.history.items():
942                 if _addr == addr: continue
943                 if _hist == ['*']: continue
944                 _tx_hist = map(lambda x:x[0], _hist)
945                 if tx_hash in _tx_hist:
946                     found = True
947                     break
948
949             if not found:
950                 tx = self.transactions.get(tx_hash)
951                 # tx might not be there
952                 if not tx: continue
953                 
954                 # already verified?
955                 if self.verifier.get_height(tx_hash):
956                     continue
957                 # unconfirmed tx
958                 print_error("new history is orphaning transaction:", tx_hash)
959                 # check that all outputs are not mine, request histories
960                 ext_requests = []
961                 for _addr, _v in tx.outputs:
962                     # assert not self.is_mine(_addr)
963                     ext_requests.append( ('blockchain.address.get_history', [_addr]) )
964
965                 ext_h = self.interface.synchronous_get(ext_requests)
966                 print_error("sync:", ext_requests, ext_h)
967                 height = None
968                 for h in ext_h:
969                     if h == ['*']: continue
970                     for item in h:
971                         if item.get('tx_hash') == tx_hash:
972                             height = item.get('height')
973                 if height:
974                     print_error("found height for", tx_hash, height)
975                     self.verifier.add(tx_hash, height)
976                 else:
977                     print_error("removing orphaned tx from history", tx_hash)
978                     self.transactions.pop(tx_hash)
979
980         return True
981
982
983
984     def check_new_tx(self, tx_hash, tx):
985         # 1 check that tx is referenced in addr_history. 
986         addresses = []
987         for addr, hist in self.history.items():
988             if hist == ['*']:continue
989             for txh, height in hist:
990                 if txh == tx_hash: 
991                     addresses.append(addr)
992
993         if not addresses:
994             return False
995
996         # 2 check that referencing addresses are in the tx
997         for addr in addresses:
998             if not tx.has_address(addr):
999                 return False
1000
1001         return True
1002
1003
1004
1005
1006 class WalletSynchronizer(threading.Thread):
1007
1008
1009     def __init__(self, wallet, config):
1010         threading.Thread.__init__(self)
1011         self.daemon = True
1012         self.wallet = wallet
1013         self.interface = self.wallet.interface
1014         self.interface.register_channel('synchronizer')
1015         self.wallet.interface.register_callback('connected', lambda: self.wallet.set_up_to_date(False))
1016         self.was_updated = True
1017         self.running = False
1018         self.lock = threading.Lock()
1019
1020     def stop(self):
1021         with self.lock: self.running = False
1022         self.interface.poke('synchronizer')
1023
1024     def is_running(self):
1025         with self.lock: return self.running
1026
1027     
1028     def subscribe_to_addresses(self, addresses):
1029         messages = []
1030         for addr in addresses:
1031             messages.append(('blockchain.address.subscribe', [addr]))
1032         self.interface.send( messages, 'synchronizer')
1033
1034
1035     def run(self):
1036         with self.lock: self.running = True
1037
1038         requested_tx = []
1039         missing_tx = []
1040         requested_histories = {}
1041
1042         # request any missing transactions
1043         for history in self.wallet.history.values():
1044             if history == ['*']: continue
1045             for tx_hash, tx_height in history:
1046                 if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
1047                     missing_tx.append( (tx_hash, tx_height) )
1048         print_error("missing tx", missing_tx)
1049
1050         # wait until we are connected, in case the user is not connected
1051         while not self.interface.is_connected:
1052             time.sleep(1)
1053         
1054         # subscriptions
1055         self.subscribe_to_addresses(self.wallet.addresses(True))
1056
1057         while self.is_running():
1058             # 1. create new addresses
1059             new_addresses = self.wallet.synchronize()
1060
1061             # request missing addresses
1062             if new_addresses:
1063                 self.subscribe_to_addresses(new_addresses)
1064
1065             # request missing transactions
1066             for tx_hash, tx_height in missing_tx:
1067                 if (tx_hash, tx_height) not in requested_tx:
1068                     self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
1069                     requested_tx.append( (tx_hash, tx_height) )
1070             missing_tx = []
1071
1072             # detect if situation has changed
1073             if not self.interface.is_up_to_date('synchronizer'):
1074                 if self.wallet.is_up_to_date():
1075                     self.wallet.set_up_to_date(False)
1076                     self.was_updated = True
1077             else:
1078                 if not self.wallet.is_up_to_date():
1079                     self.wallet.set_up_to_date(True)
1080                     self.was_updated = True
1081
1082             if self.was_updated:
1083                 self.interface.trigger_callback('updated')
1084                 self.was_updated = False
1085
1086             # 2. get a response
1087             r = self.interface.get_response('synchronizer')
1088
1089             # poke sends None. (needed during stop)
1090             if not r: continue
1091
1092             # 3. handle response
1093             method = r['method']
1094             params = r['params']
1095             result = r.get('result')
1096             error = r.get('error')
1097             if error:
1098                 print "error", r
1099                 continue
1100
1101             if method == 'blockchain.address.subscribe':
1102                 addr = params[0]
1103                 if self.wallet.get_status(self.wallet.get_history(addr)) != result:
1104                     if requested_histories.get(addr) is None:
1105                         self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
1106                         requested_histories[addr] = result
1107
1108             elif method == 'blockchain.address.get_history':
1109                 addr = params[0]
1110                 print_error("receiving history", addr, result)
1111                 if result == ['*']:
1112                     assert requested_histories.pop(addr) == '*'
1113                     self.wallet.receive_history_callback(addr, result)
1114                 else:
1115                     hist = []
1116                     # check that txids are unique
1117                     txids = []
1118                     for item in result:
1119                         tx_hash = item['tx_hash']
1120                         if tx_hash not in txids:
1121                             txids.append(tx_hash)
1122                             hist.append( (tx_hash, item['height']) )
1123
1124                     if len(hist) != len(result):
1125                         raise BaseException("error: server sent history with non-unique txid", result)
1126
1127                     # check that the status corresponds to what was announced
1128                     rs = requested_histories.pop(addr)
1129                     if self.wallet.get_status(hist) != rs:
1130                         raise BaseException("error: status mismatch: %s"%addr)
1131                 
1132                     # store received history
1133                     self.wallet.receive_history_callback(addr, hist)
1134
1135                     # request transactions that we don't have 
1136                     for tx_hash, tx_height in hist:
1137                         if self.wallet.transactions.get(tx_hash) is None:
1138                             if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
1139                                 missing_tx.append( (tx_hash, tx_height) )
1140
1141             elif method == 'blockchain.transaction.get':
1142                 tx_hash = params[0]
1143                 tx_height = params[1]
1144                 assert tx_hash == hash_encode(Hash(result.decode('hex')))
1145                 tx = Transaction(result)
1146                 self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
1147                 self.was_updated = True
1148                 requested_tx.remove( (tx_hash, tx_height) )
1149                 print_error("received tx:", tx_hash, len(tx.raw))
1150
1151             elif method == 'blockchain.transaction.broadcast':
1152                 self.wallet.tx_result = result
1153                 self.wallet.tx_event.set()
1154
1155             else:
1156                 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
1157
1158             if self.was_updated and not requested_tx:
1159                 self.interface.trigger_callback('updated')
1160                 self.was_updated = False
1161
1162