232e2025a3c2a42d50e582ef51e7fdbc24584b39
[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 get_unspent_coins(self, domain=None):
469         coins = []
470         if domain is None: domain = self.all_addresses()
471         for addr in domain:
472             h = self.history.get(addr, [])
473             if h == ['*']: continue
474             for tx_hash, tx_height in h:
475                 tx = self.transactions.get(tx_hash)
476                 for output in tx.d.get('outputs'):
477                     if output.get('address') != addr: continue
478                     key = tx_hash + ":%d" % output.get('index')
479                     if key in self.spent_outputs: continue
480                     output['tx_hash'] = tx_hash
481                     coins.append(output)
482         return coins
483
484
485
486     def choose_tx_inputs( self, amount, fixed_fee, from_addr = None ):
487         """ todo: minimize tx size """
488         total = 0
489         fee = self.fee if fixed_fee is None else fixed_fee
490
491         coins = []
492         prioritized_coins = []
493         domain = [from_addr] if from_addr else self.all_addresses()
494         for i in self.frozen_addresses:
495             if i in domain: domain.remove(i)
496
497         for i in self.prioritized_addresses:
498             if i in domain: domain.remove(i)
499
500         coins = self.get_unspent_coins(domain)
501         prioritized_coins = self.get_unspent_coins(self.prioritized_addresses)
502
503         inputs = []
504         coins = prioritized_coins + coins
505
506         for item in coins: 
507             addr = item.get('address')
508             v = item.get('value')
509             total += v
510             item['pubkeysig'] = [(None, None)]
511             inputs.append( item )
512             fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee
513             if total >= amount + fee: break
514         else:
515             #print "not enough funds: %s %s"%(format_satoshis(total), format_satoshis(fee))
516             inputs = []
517         return inputs, total, fee
518
519     def add_tx_change( self, outputs, amount, fee, total, change_addr=None ):
520         change_amount = total - ( amount + fee )
521         if change_amount != 0:
522             # normally, the update thread should ensure that the last change address is unused
523             if not change_addr:
524                 change_addr = self.change_addresses[-self.gap_limit_for_change]
525             # Insert the change output at a random position in the outputs
526             posn = random.randint(0, len(outputs))
527             outputs[posn:posn] = [( change_addr,  change_amount)]
528         return outputs
529
530
531     def get_history(self, address):
532         with self.lock:
533             return self.history.get(address)
534
535     def get_status(self, h):
536         if not h: return None
537         if h == ['*']: return '*'
538         status = ''
539         for tx_hash, height in h:
540             status += tx_hash + ':%d:' % height
541         return hashlib.sha256( status ).digest().encode('hex')
542
543
544
545     def receive_tx_callback(self, tx_hash, tx, tx_height):
546
547         if not self.check_new_tx(tx_hash, tx):
548             raise BaseException("error: received transaction is not consistent with history"%tx_hash)
549
550         with self.lock:
551             self.transactions[tx_hash] = tx
552             self.tx_height[tx_hash] = tx_height
553
554         #tx_height = tx.get('height')
555         if self.verifier and tx_height>0: 
556             self.verifier.add(tx_hash, tx_height)
557
558         self.update_tx_outputs(tx_hash)
559
560         self.save()
561
562
563     def receive_history_callback(self, addr, hist):
564
565         if not self.check_new_history(addr, hist):
566             raise BaseException("error: received history for %s is not consistent with known transactions"%addr)
567             
568         with self.lock:
569             self.history[addr] = hist
570             self.save()
571
572         if hist != ['*']:
573             for tx_hash, tx_height in hist:
574                 if tx_height>0:
575                     # add it in case it was previously unconfirmed
576                     if self.verifier: self.verifier.add(tx_hash, tx_height)
577                     # set the height in case it changed
578                     txh = self.tx_height.get(tx_hash)
579                     if txh is not None and txh != tx_height:
580                         print_error( "changing height for tx", tx_hash )
581                         self.tx_height[tx_hash] = tx_height
582
583
584     def get_tx_history(self):
585         with self.lock:
586             history = self.transactions.items()
587         history.sort(key = lambda x: self.tx_height.get(x[0]) if self.tx_height.get(x[0]) else 1e12)
588         result = []
589     
590         balance = 0
591         for tx_hash, tx in history:
592             is_mine, v, fee = self.get_tx_value(tx)
593             if v is not None: balance += v
594         c, u = self.get_balance()
595
596         if balance != c+u:
597             v_str = format_satoshis( c+u - balance, True, self.num_zeros)
598             result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
599
600         balance = c + u - balance
601         for tx_hash, tx in history:
602             conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
603             is_mine, value, fee = self.get_tx_value(tx)
604             if value is not None:
605                 balance += value
606
607             result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
608
609         return result
610
611
612     def get_label(self, tx_hash):
613         label = self.labels.get(tx_hash)
614         is_default = (label == '') or (label is None)
615         if is_default: label = self.get_default_label(tx_hash)
616         return label, is_default
617
618
619     def get_default_label(self, tx_hash):
620         tx = self.transactions.get(tx_hash)
621         default_label = ''
622         if tx:
623             is_mine, _, _ = self.get_tx_value(tx)
624             if is_mine:
625                 for o in tx.outputs:
626                     o_addr, _ = o
627                     if not self.is_mine(o_addr):
628                         try:
629                             default_label = self.labels[o_addr]
630                         except KeyError:
631                             default_label = o_addr
632             else:
633                 for o in tx.outputs:
634                     o_addr, _ = o
635                     if self.is_mine(o_addr) and not self.is_change(o_addr):
636                         break
637                 else:
638                     for o in tx.outputs:
639                         o_addr, _ = o
640                         if self.is_mine(o_addr):
641                             break
642                     else:
643                         o_addr = None
644
645                 if o_addr:
646                     dest_label = self.labels.get(o_addr)
647                     try:
648                         default_label = self.labels[o_addr]
649                     except KeyError:
650                         default_label = o_addr
651
652         return default_label
653
654
655     def mktx(self, outputs, label, password, fee=None, change_addr=None, from_addr= None):
656
657         for address, x in outputs:
658             assert self.is_valid(address)
659
660         amount = sum( map(lambda x:x[1], outputs) )
661         inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr )
662         if not inputs:
663             raise ValueError("Not enough funds")
664
665         if not self.use_change and not change_addr:
666             change_addr = inputs[-1]['address']
667             print_error( "Sending change to", change_addr )
668         outputs = self.add_tx_change(outputs, amount, fee, total, change_addr)
669
670         if not self.seed:
671             return repr({'inputs':inputs, 'outputs':outputs})
672         
673         tx = self.signed_tx(inputs, outputs, password)
674
675         for address, x in outputs:
676             if address not in self.addressbook and not self.is_mine(address):
677                 self.addressbook.append(address)
678
679         if label: 
680             tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
681             self.labels[tx_hash] = label
682
683         return tx
684
685     def signed_tx(self, inputs, outputs, password):
686         tx = Transaction.from_io(inputs, outputs)
687         private_keys = {}
688         for txin in tx.inputs:
689             addr = txin['address']
690             sec = self.get_private_key(addr, password)
691             private_keys[addr] = sec
692         tx.sign(private_keys)
693         return str(tx)
694
695     def sendtx(self, tx):
696         # synchronous
697         h = self.send_tx(tx)
698         self.tx_event.wait()
699         return self.receive_tx(h)
700
701     def send_tx(self, tx):
702         # asynchronous
703         self.tx_event.clear()
704         tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
705         self.interface.send([('blockchain.transaction.broadcast', [tx])], 'synchronizer')
706         return tx_hash
707
708     def receive_tx(self,tx_hash):
709         out = self.tx_result 
710         if out != tx_hash:
711             return False, "error: " + out
712         if self.receipt:
713             self.receipts[tx_hash] = self.receipt
714             self.receipt = None
715         return True, out
716
717
718     def read_alias(self, alias):
719         # this might not be the right place for this function.
720         import urllib
721
722         m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias)
723         m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias)
724         if m1:
725             url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) 
726         elif m2:
727             url = 'https://' + alias + '/bitcoin.id'
728         else:
729             return ''
730         try:
731             lines = urllib.urlopen(url).readlines()
732         except:
733             return ''
734
735         # line 0
736         line = lines[0].strip().split(':')
737         if len(line) == 1:
738             auth_name = None
739             target = signing_addr = line[0]
740         else:
741             target, auth_name, signing_addr, signature = line
742             msg = "alias:%s:%s:%s"%(alias,target,auth_name)
743             print msg, signature
744             EC_KEY.verify_message(signing_addr, signature, msg)
745         
746         # other lines are signed updates
747         for line in lines[1:]:
748             line = line.strip()
749             if not line: continue
750             line = line.split(':')
751             previous = target
752             print repr(line)
753             target, signature = line
754             EC_KEY.verify_message(previous, signature, "alias:%s:%s"%(alias,target))
755
756         if not self.is_valid(target):
757             raise ValueError("Invalid bitcoin address")
758
759         return target, signing_addr, auth_name
760
761     def update_password(self, seed, old_password, new_password):
762         if new_password == '': new_password = None
763         self.use_encryption = (new_password != None)
764         self.seed = pw_encode( seed, new_password)
765         self.config.set_key('seed', self.seed, True)
766         for k in self.imported_keys.keys():
767             a = self.imported_keys[k]
768             b = pw_decode(a, old_password)
769             c = pw_encode(b, new_password)
770             self.imported_keys[k] = c
771         self.save()
772
773     def get_alias(self, alias, interactive = False, show_message=None, question = None):
774         try:
775             target, signing_address, auth_name = self.read_alias(alias)
776         except BaseException, e:
777             # raise exception if verify fails (verify the chain)
778             if interactive:
779                 show_message("Alias error: " + str(e))
780             return
781
782         print target, signing_address, auth_name
783
784         if auth_name is None:
785             a = self.aliases.get(alias)
786             if not a:
787                 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)
788                 if interactive and question( msg ):
789                     self.aliases[alias] = (signing_address, target)
790                 else:
791                     target = None
792             else:
793                 if signing_address != a[0]:
794                     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
795                     if interactive and question( msg ):
796                         self.aliases[alias] = (signing_address, target)
797                     else:
798                         target = None
799         else:
800             if signing_address not in self.authorities.keys():
801                 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)
802                 if interactive and question( msg ):
803                     self.authorities[signing_address] = auth_name
804                 else:
805                     target = None
806
807         if target:
808             self.aliases[alias] = (signing_address, target)
809             
810         return target
811
812
813     def parse_url(self, url, show_message, question):
814         o = url[8:].split('?')
815         address = o[0]
816         if len(o)>1:
817             params = o[1].split('&')
818         else:
819             params = []
820
821         amount = label = message = signature = identity = ''
822         for p in params:
823             k,v = p.split('=')
824             uv = urldecode(v)
825             if k == 'amount': amount = uv
826             elif k == 'message': message = uv
827             elif k == 'label': label = uv
828             elif k == 'signature':
829                 identity, signature = uv.split(':')
830                 url = url.replace('&%s=%s'%(k,v),'')
831             else: 
832                 print k,v
833
834         if label and self.labels.get(address) != label:
835             if question('Give label "%s" to address %s ?'%(label,address)):
836                 if address not in self.addressbook and address not in self.all_addresses(): 
837                     self.addressbook.append(address)
838                 self.labels[address] = label
839
840         if signature:
841             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity):
842                 signing_address = self.get_alias(identity, True, show_message, question)
843             elif self.is_valid(identity):
844                 signing_address = identity
845             else:
846                 signing_address = None
847             if not signing_address:
848                 return
849             try:
850                 EC_KEY.verify_message(signing_address, signature, url )
851                 self.receipt = (signing_address, signature, url)
852             except:
853                 show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.')
854                 address = amount = label = identity = message = ''
855
856         if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address):
857             payto_address = self.get_alias(address, True, show_message, question)
858             if payto_address:
859                 address = address + ' <' + payto_address + '>'
860
861         return address, amount, label, message, signature, identity, url
862
863
864
865     def freeze(self,addr):
866         if addr in self.all_addresses() and addr not in self.frozen_addresses:
867             self.unprioritize(addr)
868             self.frozen_addresses.append(addr)
869             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
870             return True
871         else:
872             return False
873
874     def unfreeze(self,addr):
875         if addr in self.all_addresses() and addr in self.frozen_addresses:
876             self.frozen_addresses.remove(addr)
877             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
878             return True
879         else:
880             return False
881
882     def prioritize(self,addr):
883         if addr in self.all_addresses() and addr not in self.prioritized_addresses:
884             self.unfreeze(addr)
885             self.prioritized_addresses.append(addr)
886             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
887             return True
888         else:
889             return False
890
891     def unprioritize(self,addr):
892         if addr in self.all_addresses() and addr in self.prioritized_addresses:
893             self.prioritized_addresses.remove(addr)
894             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
895             return True
896         else:
897             return False
898
899     def save(self):
900         tx = {}
901         for k,v in self.transactions.items():
902             tx[k] = str(v)
903             
904         s = {
905             'use_encryption': self.use_encryption,
906             'use_change': self.use_change,
907             'fee': self.fee,
908             'addresses': self.addresses,
909             'change_addresses': self.change_addresses,
910             'addr_history': self.history, 
911             'labels': self.labels,
912             'contacts': self.addressbook,
913             'imported_keys': self.imported_keys,
914             'aliases': self.aliases,
915             'authorities': self.authorities,
916             'receipts': self.receipts,
917             'num_zeros': self.num_zeros,
918             'frozen_addresses': self.frozen_addresses,
919             'prioritized_addresses': self.prioritized_addresses,
920             'gap_limit': self.gap_limit,
921             'transactions': tx,
922             'tx_height': self.tx_height,
923             'requested_amounts': self.requested_amounts,
924         }
925         for k, v in s.items():
926             self.config.set_key(k,v)
927         self.config.save()
928
929     def set_verifier(self, verifier):
930         self.verifier = verifier
931
932         # review stored transactions and send them to the verifier
933         # (they are not necessarily in the history, because history items might have have been pruned)
934         for tx_hash, tx in self.transactions.items():
935             tx_height = self.tx_height[tx_hash]
936             if tx_height <1:
937                 print_error( "skipping", tx_hash, tx_height )
938                 continue
939             
940             if tx_height>0:
941                 self.verifier.add(tx_hash, tx_height)
942
943         # review transactions that are in the history
944         for addr, hist in self.history.items():
945             if hist == ['*']: continue
946             for tx_hash, tx_height in hist:
947                 if tx_height>0:
948                     # add it in case it was previously unconfirmed
949                     self.verifier.add(tx_hash, tx_height)
950                     # set the height in case it changed
951                     txh = self.tx_height.get(tx_hash)
952                     if txh is not None and txh != tx_height:
953                         print_error( "changing height for tx", tx_hash )
954                         self.tx_height[tx_hash] = tx_height
955
956
957
958
959     def check_new_history(self, addr, hist):
960         
961         # check that all tx in hist are relevant
962         if hist != ['*']:
963             for tx_hash, height in hist:
964                 tx = self.transactions.get(tx_hash)
965                 if not tx: continue
966                 if not tx.has_address(addr):
967                     return False
968
969         # check that we are not "orphaning" a transaction
970         old_hist = self.history.get(addr,[])
971         if old_hist == ['*']: return True
972
973         for tx_hash, height in old_hist:
974             if tx_hash in map(lambda x:x[0], hist): continue
975             found = False
976             for _addr, _hist in self.history.items():
977                 if _addr == addr: continue
978                 if _hist == ['*']: continue
979                 _tx_hist = map(lambda x:x[0], _hist)
980                 if tx_hash in _tx_hist:
981                     found = True
982                     break
983
984             if not found:
985                 tx = self.transactions.get(tx_hash)
986                 # tx might not be there
987                 if not tx: continue
988                 
989                 # already verified?
990                 if self.tx_height.get(tx_hash):
991                     continue
992                 # unconfirmed tx
993                 print_error("new history is orphaning transaction:", tx_hash)
994                 # check that all outputs are not mine, request histories
995                 ext_requests = []
996                 for o in tx.get('outputs'):
997                     _addr = o.get('address')
998                     # assert not self.is_mine(_addr)
999                     ext_requests.append( ('blockchain.address.get_history', [_addr]) )
1000
1001                 ext_h = self.interface.synchronous_get(ext_requests)
1002                 height = None
1003                 for h in ext_h:
1004                     if h == ['*']: continue
1005                     for item in h:
1006                         if item.get('tx_hash') == tx_hash:
1007                             height = item.get('height')
1008                             self.tx_height[tx_hash] = height
1009                 if height:
1010                     print_error("found height for", tx_hash, height)
1011                     self.verifier.add(tx_hash, height)
1012                 else:
1013                     print_error("removing orphaned tx from history", tx_hash)
1014                     self.transactions.pop(tx_hash)
1015
1016         return True
1017
1018
1019
1020     def check_new_tx(self, tx_hash, tx):
1021         # 1 check that tx is referenced in addr_history. 
1022         addresses = []
1023         for addr, hist in self.history.items():
1024             if hist == ['*']:continue
1025             for txh, height in hist:
1026                 if txh == tx_hash: 
1027                     addresses.append(addr)
1028
1029         if not addresses:
1030             return False
1031
1032         # 2 check that referencing addresses are in the tx
1033         for addr in addresses:
1034             if not tx.has_address(addr):
1035                 return False
1036
1037         return True
1038
1039
1040
1041
1042 class WalletSynchronizer(threading.Thread):
1043
1044
1045     def __init__(self, wallet, config):
1046         threading.Thread.__init__(self)
1047         self.daemon = True
1048         self.wallet = wallet
1049         self.interface = self.wallet.interface
1050         self.interface.register_channel('synchronizer')
1051         self.wallet.interface.register_callback('connected', lambda: self.wallet.set_up_to_date(False))
1052         self.wallet.interface.register_callback('connected', lambda: self.interface.send([('server.banner',[])],'synchronizer') )
1053         self.was_updated = True
1054         self.running = False
1055         self.lock = threading.Lock()
1056
1057     def stop(self):
1058         with self.lock: self.running = False
1059         self.interface.poke('synchronizer')
1060
1061     def is_running(self):
1062         with self.lock: return self.running
1063
1064     def synchronize_wallet(self):
1065         new_addresses = self.wallet.synchronize()
1066         if new_addresses:
1067             self.subscribe_to_addresses(new_addresses)
1068             self.wallet.up_to_date = False
1069             return
1070             
1071         if not self.interface.is_up_to_date('synchronizer'):
1072             if self.wallet.is_up_to_date():
1073                 self.wallet.set_up_to_date(False)
1074                 self.was_updated = True
1075             return
1076
1077         self.wallet.set_up_to_date(True)
1078         self.was_updated = True
1079
1080     
1081     def subscribe_to_addresses(self, addresses):
1082         messages = []
1083         for addr in addresses:
1084             messages.append(('blockchain.address.subscribe', [addr]))
1085         self.interface.send( messages, 'synchronizer')
1086
1087
1088     def run(self):
1089         with self.lock: self.running = True
1090
1091         requested_tx = []
1092         missing_tx = []
1093         requested_histories = {}
1094
1095         # request any missing transactions
1096         for history in self.wallet.history.values():
1097             if history == ['*']: continue
1098             for tx_hash, tx_height in history:
1099                 if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
1100                     missing_tx.append( (tx_hash, tx_height) )
1101         print_error("missing tx", missing_tx)
1102
1103         # wait until we are connected, in case the user is not connected
1104         while not self.interface.is_connected:
1105             time.sleep(1)
1106         
1107         # request banner, because 'connected' event happens before this thread is started
1108         self.interface.send([('server.banner',[])],'synchronizer')
1109
1110         # subscriptions
1111         self.subscribe_to_addresses(self.wallet.all_addresses())
1112
1113         while self.is_running():
1114             # 1. send new requests
1115             self.synchronize_wallet()
1116
1117             for tx_hash, tx_height in missing_tx:
1118                 if (tx_hash, tx_height) not in requested_tx:
1119                     self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
1120                     requested_tx.append( (tx_hash, tx_height) )
1121             missing_tx = []
1122
1123             if self.was_updated:
1124                 self.interface.trigger_callback('updated')
1125                 self.was_updated = False
1126
1127             # 2. get a response
1128             r = self.interface.get_response('synchronizer')
1129
1130             # poke sends None. (needed during stop)
1131             if not r: continue
1132
1133             # 3. handle response
1134             method = r['method']
1135             params = r['params']
1136             result = r.get('result')
1137             error = r.get('error')
1138             if error:
1139                 print "error", r
1140                 continue
1141
1142             if method == 'blockchain.address.subscribe':
1143                 addr = params[0]
1144                 if self.wallet.get_status(self.wallet.get_history(addr)) != result:
1145                     if requested_histories.get(addr) is None:
1146                         self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
1147                         requested_histories[addr] = result
1148
1149             elif method == 'blockchain.address.get_history':
1150                 addr = params[0]
1151                 print_error("receiving history", addr, result)
1152                 if result == ['*']:
1153                     assert requested_histories.pop(addr) == '*'
1154                     self.wallet.receive_history_callback(addr, result)
1155                 else:
1156                     hist = []
1157                     # check that txids are unique
1158                     txids = []
1159                     for item in result:
1160                         tx_hash = item['tx_hash']
1161                         if tx_hash not in txids:
1162                             txids.append(tx_hash)
1163                             hist.append( (tx_hash, item['height']) )
1164
1165                     if len(hist) != len(result):
1166                         raise BaseException("error: server sent history with non-unique txid", result)
1167
1168                     # check that the status corresponds to what was announced
1169                     rs = requested_histories.pop(addr)
1170                     if self.wallet.get_status(hist) != rs:
1171                         raise BaseException("error: status mismatch: %s"%addr)
1172                 
1173                     # store received history
1174                     self.wallet.receive_history_callback(addr, hist)
1175
1176                     # request transactions that we don't have 
1177                     for tx_hash, tx_height in hist:
1178                         if self.wallet.transactions.get(tx_hash) is None:
1179                             if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
1180                                 missing_tx.append( (tx_hash, tx_height) )
1181
1182             elif method == 'blockchain.transaction.get':
1183                 tx_hash = params[0]
1184                 tx_height = params[1]
1185                 assert tx_hash == hash_encode(Hash(result.decode('hex')))
1186                 tx = Transaction(result)
1187                 self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
1188                 self.was_updated = True
1189                 requested_tx.remove( (tx_hash, tx_height) )
1190                 print_error("received tx:", tx)
1191
1192             elif method == 'blockchain.transaction.broadcast':
1193                 self.wallet.tx_result = result
1194                 self.wallet.tx_event.set()
1195
1196             elif method == 'server.banner':
1197                 self.wallet.banner = result
1198                 self.interface.trigger_callback('banner')
1199             else:
1200                 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
1201
1202             if self.was_updated and not requested_tx:
1203                 self.interface.trigger_callback('updated')
1204                 self.was_updated = False
1205
1206