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