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