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