check consistency of results received by get_history
[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_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
47 from version import ELECTRUM_VERSION, SEED_VERSION
48
49
50 class Wallet:
51     def __init__(self, config={}):
52
53         self.config = config
54         self.electrum_version = ELECTRUM_VERSION
55
56         # saved fields
57         self.seed_version          = config.get('seed_version', SEED_VERSION)
58         self.gap_limit             = config.get('gap_limit', 5)
59         self.use_change            = config.get('use_change',True)
60         self.fee                   = int(config.get('fee',100000))
61         self.num_zeros             = int(config.get('num_zeros',0))
62         self.master_public_key     = config.get('master_public_key','')
63         self.use_encryption        = config.get('use_encryption', False)
64         self.addresses             = config.get('addresses', [])          # receiving addresses visible for user
65         self.change_addresses      = config.get('change_addresses', [])   # addresses used as change
66         self.seed                  = config.get('seed', '')               # encrypted
67         self.labels                = config.get('labels',{})              # labels for addresses and transactions
68         self.aliases               = config.get('aliases', {})            # aliases for addresses
69         self.authorities           = config.get('authorities', {})        # trusted addresses
70         self.frozen_addresses      = config.get('frozen_addresses',[])
71         self.prioritized_addresses = config.get('prioritized_addresses',[])
72         self.receipts              = config.get('receipts',{})            # signed URIs
73         self.addressbook           = config.get('contacts', [])           # outgoing addresses, for payments
74         self.imported_keys         = config.get('imported_keys',{})
75         self.history               = config.get('addr_history',{})        # address -> list(txid, height)
76         self.transactions          = config.get('transactions',{})        # txid -> deserialised
77
78         # not saved
79         self.prevout_values = {}
80         self.spent_outputs = []
81         self.receipt = None          # next receipt
82         self.banner = ''
83
84         # spv
85         self.verifier = None
86
87         # there is a difference between wallet.up_to_date and interface.is_up_to_date()
88         # interface.is_up_to_date() returns true when all requests have been answered and processed
89         # wallet.up_to_date is true when the wallet is synchronized (stronger requirement)
90         self.up_to_date_event = threading.Event()
91         self.up_to_date_event.clear()
92         self.up_to_date = False
93         self.lock = threading.Lock()
94         self.tx_event = threading.Event()
95
96         if self.seed_version != SEED_VERSION:
97             raise ValueError("This wallet seed is deprecated. Please run upgrade.py for a diagnostic.")
98
99         for tx_hash in self.transactions.keys():
100             self.update_tx_outputs(tx_hash)
101
102
103     def init_up_to_date(self):
104         self.up_to_date_event.clear()
105         self.up_to_date = False
106
107
108     def import_key(self, keypair, password):
109         address, key = keypair.split(':')
110         if not self.is_valid(address):
111             raise BaseException('Invalid Bitcoin address')
112         if address in self.all_addresses():
113             raise BaseException('Address already in wallet')
114         b = ASecretToSecret( key )
115         if not b: 
116             raise BaseException('Unsupported key format')
117         secexp = int( b.encode('hex'), 16)
118         private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve=SECP256k1 )
119         # sanity check
120         public_key = private_key.get_verifying_key()
121         if not address == public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ):
122             raise BaseException('Address does not match private key')
123         self.imported_keys[address] = self.pw_encode( key, password )
124
125
126     def new_seed(self, password):
127         seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
128         #self.init_mpk(seed)
129         # encrypt
130         self.seed = self.pw_encode( seed, password )
131
132
133     def init_mpk(self,seed):
134         # public key
135         curve = SECP256k1
136         secexp = self.stretch_key(seed)
137         master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
138         self.master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
139
140     def all_addresses(self):
141         return self.addresses + self.change_addresses + self.imported_keys.keys()
142
143     def is_mine(self, address):
144         return address in self.all_addresses()
145
146     def is_change(self, address):
147         return address in self.change_addresses
148
149     def is_valid(self,addr):
150         ADDRESS_RE = re.compile('[1-9A-HJ-NP-Za-km-z]{26,}\\Z')
151         if not ADDRESS_RE.match(addr): return False
152         try:
153             h = bc_address_to_hash_160(addr)
154         except:
155             return False
156         return addr == hash_160_to_bc_address(h)
157
158     def stretch_key(self,seed):
159         oldseed = seed
160         for i in range(100000):
161             seed = hashlib.sha256(seed + oldseed).digest()
162         return string_to_number( seed )
163
164     def get_sequence(self,n,for_change):
165         return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key.decode('hex') ) )
166
167     def get_private_key_base58(self, address, password):
168         pk = self.get_private_key(address, password)
169         if pk is None: return None
170         return SecretToASecret( pk )
171
172     def get_private_key(self, address, password):
173         """  Privatekey(type,n) = Master_private_key + H(n|S|type)  """
174         order = generator_secp256k1.order()
175         
176         if address in self.imported_keys.keys():
177             b = self.pw_decode( self.imported_keys[address], password )
178             if not b: return None
179             b = ASecretToSecret( b )
180             secexp = int( b.encode('hex'), 16)
181         else:
182             if address in self.addresses:
183                 n = self.addresses.index(address)
184                 for_change = False
185             elif address in self.change_addresses:
186                 n = self.change_addresses.index(address)
187                 for_change = True
188             else:
189                 raise BaseException("unknown address")
190             try:
191                 seed = self.pw_decode( self.seed, password)
192             except:
193                 raise BaseException("Invalid password")
194             if not seed: return None
195             secexp = self.stretch_key(seed)
196             secexp = ( secexp + self.get_sequence(n,for_change) ) % order
197
198         pk = number_to_string(secexp,order)
199         return pk
200
201     def msg_magic(self, message):
202         return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message
203
204     def sign_message(self, address, message, password):
205         private_key = ecdsa.SigningKey.from_string( self.get_private_key(address, password), curve = SECP256k1 )
206         public_key = private_key.get_verifying_key()
207         signature = private_key.sign_digest( Hash( self.msg_magic( message ) ), sigencode = ecdsa.util.sigencode_string )
208         assert public_key.verify_digest( signature, Hash( self.msg_magic( message ) ), sigdecode = ecdsa.util.sigdecode_string)
209         for i in range(4):
210             sig = base64.b64encode( chr(27+i) + signature )
211             try:
212                 self.verify_message( address, sig, message)
213                 return sig
214             except:
215                 continue
216         else:
217             raise BaseException("error: cannot sign message")
218
219
220     def verify_message(self, address, signature, message):
221         """ See http://www.secg.org/download/aid-780/sec1-v2.pdf for the math """
222         from ecdsa import numbertheory, ellipticcurve, util
223         import msqr
224         curve = curve_secp256k1
225         G = generator_secp256k1
226         order = G.order()
227         # extract r,s from signature
228         sig = base64.b64decode(signature)
229         if len(sig) != 65: raise BaseException("Wrong encoding")
230         r,s = util.sigdecode_string(sig[1:], order)
231         nV = ord(sig[0])
232         if nV < 27 or nV >= 35:
233             raise BaseException("Bad encoding")
234         if nV >= 31:
235             compressed = True
236             nV -= 4
237         else:
238             compressed = False
239
240         recid = nV - 27
241         # 1.1
242         x = r + (recid/2) * order
243         # 1.3
244         alpha = ( x * x * x  + curve.a() * x + curve.b() ) % curve.p()
245         beta = msqr.modular_sqrt(alpha, curve.p())
246         y = beta if (beta - recid) % 2 == 0 else curve.p() - beta
247         # 1.4 the constructor checks that nR is at infinity
248         R = ellipticcurve.Point(curve, x, y, order)
249         # 1.5 compute e from message:
250         h = Hash( self.msg_magic( message ) )
251         e = string_to_number(h)
252         minus_e = -e % order
253         # 1.6 compute Q = r^-1 (sR - eG)
254         inv_r = numbertheory.inverse_mod(r,order)
255         Q = inv_r * ( s * R + minus_e * G )
256         public_key = ecdsa.VerifyingKey.from_public_point( Q, curve = SECP256k1 )
257         # check that Q is the public key
258         public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string)
259         # check that we get the original signing address
260         addr = public_key_to_bc_address( encode_point(public_key, compressed) )
261         if address != addr:
262             raise BaseException("Bad signature")
263     
264
265     def create_new_address(self, for_change):
266         n = len(self.change_addresses) if for_change else len(self.addresses)
267         address = self.get_new_address(n, for_change)
268         if for_change:
269             self.change_addresses.append(address)
270         else:
271             self.addresses.append(address)
272         self.history[address] = []
273         return address
274         
275     def get_new_address(self, n, for_change):
276         """   Publickey(type,n) = Master_public_key + H(n|S|type)*point  """
277         curve = SECP256k1
278         z = self.get_sequence(n, for_change)
279         master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key.decode('hex'), curve = SECP256k1 )
280         pubkey_point = master_public_key.pubkey.point + z*curve.generator
281         public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 )
282         address = public_key_to_bc_address( '04'.decode('hex') + public_key2.to_string() )
283         print address
284         return address
285                                                                       
286
287     def change_gap_limit(self, value):
288         if value >= self.gap_limit:
289             self.gap_limit = value
290             self.save()
291             self.interface.poke('synchronizer')
292             return True
293
294         elif value >= self.min_acceptable_gap():
295             k = self.num_unused_trailing_addresses()
296             n = len(self.addresses) - k + value
297             self.addresses = self.addresses[0:n]
298             self.gap_limit = value
299             self.save()
300             return True
301         else:
302             return False
303
304     def num_unused_trailing_addresses(self):
305         k = 0
306         for a in self.addresses[::-1]:
307             if self.history.get(a):break
308             k = k + 1
309         return k
310
311     def min_acceptable_gap(self):
312         # fixme: this assumes wallet is synchronized
313         n = 0
314         nmax = 0
315         k = self.num_unused_trailing_addresses()
316         for a in self.addresses[0:-k]:
317             if self.history.get(a):
318                 n = 0
319             else:
320                 n += 1
321                 if n > nmax: nmax = n
322         return nmax + 1
323
324
325     def synchronize(self):
326         if not self.master_public_key:
327             return []
328
329         new_addresses = []
330         while True:
331             if self.change_addresses == []:
332                 new_addresses.append( self.create_new_address(True) )
333                 continue
334             a = self.change_addresses[-1]
335             if self.history.get(a):
336                 new_addresses.append( self.create_new_address(True) )
337             else:
338                 break
339
340         n = self.gap_limit
341         while True:
342             if len(self.addresses) < n:
343                 new_addresses.append( self.create_new_address(False) )
344                 continue
345             if map( lambda a: self.history.get(a, []), self.addresses[-n:] ) == n*[[]]:
346                 break
347             else:
348                 new_addresses.append( self.create_new_address(False) )
349
350         return new_addresses
351
352
353     def is_found(self):
354         return (len(self.change_addresses) > 1 ) or ( len(self.addresses) > self.gap_limit )
355
356     def fill_addressbook(self):
357         for tx_hash, tx in self.transactions.items():
358             if self.get_tx_value(tx_hash)<0:
359                 for o in tx['outputs']:
360                     addr = o.get('address')
361                     if not self.is_mine(addr) and addr not in self.addressbook:
362                         self.addressbook.append(addr)
363         # redo labels
364         # self.update_tx_labels()
365
366
367     def get_address_flags(self, addr):
368         flags = "C" if self.is_change(addr) else "I" if addr in self.imported_keys.keys() else "-" 
369         flags += "F" if addr in self.frozen_addresses else "P" if addr in self.prioritized_addresses else "-"
370         return flags
371         
372
373     def get_tx_value(self, tx_hash, addresses = None):
374         # return the balance for that tx
375         if addresses is None: addresses = self.all_addresses()
376         with self.lock:
377             v = 0
378             d = self.transactions.get(tx_hash)
379             if not d: return 0
380             for item in d.get('inputs'):
381                 addr = item.get('address')
382                 if addr in addresses:
383                     key = item['prevout_hash']  + ':%d'%item['prevout_n']
384                     value = self.prevout_values[ key ]
385                     v -= value
386             for item in d.get('outputs'):
387                 addr = item.get('address')
388                 if addr in addresses: 
389                     value = item.get('value')
390                     v += value 
391             return v
392
393
394     
395     def update_tx_outputs(self, tx_hash):
396         tx = self.transactions.get(tx_hash)
397         for item in tx.get('outputs'):
398             value = item.get('value')
399             key = tx_hash+ ':%d'%item.get('index')
400             with self.lock:
401                 self.prevout_values[key] = value 
402
403         for item in tx.get('inputs'):
404             if self.is_mine(item.get('address')):
405                 key = item['prevout_hash'] + ':%d'%item['prevout_n']
406                 self.spent_outputs.append(key)
407
408
409     def get_addr_balance(self, addr):
410         assert self.is_mine(addr)
411         h = self.history.get(addr,[])
412         c = u = 0
413         for tx_hash, tx_height in h:
414             v = self.get_tx_value(tx_hash, [addr])
415             if tx_height:
416                 c += v
417             else:
418                 u += v
419         return c, u
420
421     def get_balance(self):
422         conf = unconf = 0
423         for addr in self.all_addresses(): 
424             c, u = self.get_addr_balance(addr)
425             conf += c
426             unconf += u
427         return conf, unconf
428
429
430     def choose_tx_inputs( self, amount, fixed_fee, from_addr = None ):
431         """ todo: minimize tx size """
432         total = 0
433         fee = self.fee if fixed_fee is None else fixed_fee
434
435         coins = []
436         prioritized_coins = []
437         domain = [from_addr] if from_addr else self.all_addresses()
438         for i in self.frozen_addresses:
439             if i in domain: domain.remove(i)
440
441         for i in self.prioritized_addresses:
442             if i in domain: domain.remove(i)
443
444         for addr in domain:
445             h = self.history.get(addr, [])
446             for tx_hash, tx_height, in h:
447                 tx = self.transactions.get(tx_hash)
448                 for output in tx.get('outputs'):
449                     if output.get('address') != addr: continue
450                     key = tx_hash + ":%d" % output.get('index')
451                     if key in self.spent_outputs: continue
452                     output['tx_hash'] = tx_hash
453                     coins.append(output)
454
455         #coins = sorted( coins, key = lambda x: x[1]['timestamp'] )
456
457         for addr in self.prioritized_addresses:
458             h = self.history.get(addr, [])
459             for tx_hash, tx_height, in h:
460                 for output in tx.get('outputs'):
461                     if output.get('address') != addr: continue
462                     key = tx_hash + ":%d" % output.get('index')
463                     if key in self.spent_outputs: continue
464                     output['tx_hash'] = tx_hash
465                     prioritized_coins.append(output)
466
467         #prioritized_coins = sorted( prioritized_coins, key = lambda x: x[1]['timestamp'] )
468
469         inputs = []
470         coins = prioritized_coins + coins
471
472         for item in coins: 
473             addr = item.get('address')
474             v = item.get('value')
475             total += v
476             inputs.append((addr, v, item['tx_hash'], item['index'], item['raw_output_script'], None, None) )
477             fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee
478             if total >= amount + fee: break
479         else:
480             #print "not enough funds: %s %s"%(format_satoshis(total), format_satoshis(fee))
481             inputs = []
482         return inputs, total, fee
483
484     def choose_tx_outputs( self, to_addr, amount, fee, total, change_addr=None ):
485         outputs = [ (to_addr, amount) ]
486         change_amount = total - ( amount + fee )
487         if change_amount != 0:
488             # normally, the update thread should ensure that the last change address is unused
489             if not change_addr:
490                 change_addr = self.change_addresses[-1]
491             outputs.append( ( change_addr,  change_amount) )
492         return outputs
493
494     def sign_inputs( self, inputs, outputs, password ):
495         s_inputs = []
496         for i in range(len(inputs)):
497             addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i]
498             private_key = ecdsa.SigningKey.from_string( self.get_private_key(addr, password), curve = SECP256k1 )
499             public_key = private_key.get_verifying_key()
500             pubkey = public_key.to_string()
501             tx = filter( raw_tx( inputs, outputs, for_sig = i ) )
502             sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
503             assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
504             s_inputs.append( (addr, v, p_hash, p_pos, p_scriptPubKey, pubkey, sig) )
505         return s_inputs
506
507     def pw_encode(self, s, password):
508         if password:
509             secret = Hash(password)
510             return EncodeAES(secret, s)
511         else:
512             return s
513
514     def pw_decode(self, s, password):
515         if password is not None:
516             secret = Hash(password)
517             d = DecodeAES(secret, s)
518             if s == self.seed:
519                 try:
520                     d.decode('hex')
521                 except:
522                     raise ValueError("Invalid password")
523             return d
524         else:
525             return s
526
527
528     def get_history(self, address):
529         with self.lock:
530             return self.history.get(address)
531
532     def get_status(self, h):
533         if not h: return None
534         status = ''
535         for tx_hash, height in h:
536             status += tx_hash + ':%d:' % height
537         return hashlib.sha256( status ).digest().encode('hex')
538
539
540
541     def receive_tx_callback(self, tx_hash, d):
542         #print "updating history for", addr
543         #with self.lock:
544         self.transactions[tx_hash] = d
545         self.update_tx_outputs(tx_hash)
546
547         self.save()
548
549
550     def receive_history_callback(self, addr, hist):
551         #print "updating history for", addr
552         with self.lock:
553             self.history[addr] = hist
554             self.save()
555             for tx_hash, tx_height in hist:
556                 if tx_height>0:
557                     self.verifier.add(tx_hash)
558
559
560     def get_tx_history(self):
561         with self.lock:
562             lines = self.transactions.values()
563
564         lines = sorted(lines, key=operator.itemgetter("timestamp"))
565         return lines
566
567     def get_transactions_at_height(self, height):
568         with self.lock:
569             values = self.transactions.values()[:]
570
571         out = []
572         for tx in values:
573             if tx['height'] == height:
574                 out.append(tx['tx_hash'])
575         return out
576
577
578     def get_label(self, tx_hash):
579         label = self.labels.get(tx_hash)
580         is_default = (label == '') or (label is None)
581         if is_default: label = self.get_default_label(tx_hash)
582         return label, is_default
583
584     def get_default_label(self, tx_hash):
585         tx = self.transactions.get(tx_hash)
586         if tx:
587             default_label = ''
588             if self.get_tx_value(tx_hash)<0:
589                 for o in tx['outputs']:
590                     o_addr = o.get('address')
591                     if not self.is_mine(o_addr):
592                         try:
593                             default_label = self.labels[o_addr]
594                         except KeyError:
595                             default_label = o_addr
596             else:
597                 for o in tx['outputs']:
598                     o_addr = o.get('address')
599                     if self.is_mine(o_addr) and not self.is_change(o_addr):
600                         break
601                 else:
602                     for o in tx['outputs']:
603                         o_addr = o.get('address')
604                         if self.is_mine(o_addr):
605                             break
606                     else:
607                         o_addr = None
608
609                 if o_addr:
610                     dest_label = self.labels.get(o_addr)
611                     try:
612                         default_label = self.labels[o_addr]
613                     except KeyError:
614                         default_label = o_addr
615
616         return default_label
617
618
619     def mktx(self, to_address, amount, label, password, fee=None, change_addr=None, from_addr= None):
620         if not self.is_valid(to_address):
621             raise ValueError("Invalid address")
622         inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr )
623         if not inputs:
624             raise ValueError("Not enough funds")
625
626         if not self.use_change and not change_addr:
627             change_addr = inputs[0][0]
628             print "Sending change to", change_addr
629
630         outputs = self.choose_tx_outputs( to_address, amount, fee, total, change_addr )
631         s_inputs = self.sign_inputs( inputs, outputs, password )
632
633         tx = filter( raw_tx( s_inputs, outputs ) )
634         if to_address not in self.addressbook:
635             self.addressbook.append(to_address)
636         if label: 
637             tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
638             self.labels[tx_hash] = label
639
640         return tx
641
642     def sendtx(self, tx):
643         # synchronous
644         h = self.send_tx(tx)
645         self.tx_event.wait()
646         self.receive_tx(h)
647
648     def send_tx(self, tx):
649         # asynchronous
650         self.tx_event.clear()
651         tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
652         self.interface.send([('blockchain.transaction.broadcast', [tx])], 'synchronizer')
653         return tx_hash
654
655     def receive_tx(self,tx_hash):
656         out = self.tx_result 
657         if out != tx_hash:
658             return False, "error: " + out
659         if self.receipt:
660             self.receipts[tx_hash] = self.receipt
661             self.receipt = None
662         return True, out
663
664
665     def read_alias(self, alias):
666         # this might not be the right place for this function.
667         import urllib
668
669         m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias)
670         m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias)
671         if m1:
672             url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) 
673         elif m2:
674             url = 'https://' + alias + '/bitcoin.id'
675         else:
676             return ''
677         try:
678             lines = urllib.urlopen(url).readlines()
679         except:
680             return ''
681
682         # line 0
683         line = lines[0].strip().split(':')
684         if len(line) == 1:
685             auth_name = None
686             target = signing_addr = line[0]
687         else:
688             target, auth_name, signing_addr, signature = line
689             msg = "alias:%s:%s:%s"%(alias,target,auth_name)
690             print msg, signature
691             self.verify_message(signing_addr, signature, msg)
692         
693         # other lines are signed updates
694         for line in lines[1:]:
695             line = line.strip()
696             if not line: continue
697             line = line.split(':')
698             previous = target
699             print repr(line)
700             target, signature = line
701             self.verify_message(previous, signature, "alias:%s:%s"%(alias,target))
702
703         if not self.is_valid(target):
704             raise ValueError("Invalid bitcoin address")
705
706         return target, signing_addr, auth_name
707
708     def update_password(self, seed, old_password, new_password):
709         if new_password == '': new_password = None
710         self.use_encryption = (new_password != None)
711         self.seed = self.pw_encode( seed, new_password)
712         for k in self.imported_keys.keys():
713             a = self.imported_keys[k]
714             b = self.pw_decode(a, old_password)
715             c = self.pw_encode(b, new_password)
716             self.imported_keys[k] = c
717         self.save()
718
719     def get_alias(self, alias, interactive = False, show_message=None, question = None):
720         try:
721             target, signing_address, auth_name = self.read_alias(alias)
722         except BaseException, e:
723             # raise exception if verify fails (verify the chain)
724             if interactive:
725                 show_message("Alias error: " + str(e))
726             return
727
728         print target, signing_address, auth_name
729
730         if auth_name is None:
731             a = self.aliases.get(alias)
732             if not a:
733                 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)
734                 if interactive and question( msg ):
735                     self.aliases[alias] = (signing_address, target)
736                 else:
737                     target = None
738             else:
739                 if signing_address != a[0]:
740                     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
741                     if interactive and question( msg ):
742                         self.aliases[alias] = (signing_address, target)
743                     else:
744                         target = None
745         else:
746             if signing_address not in self.authorities.keys():
747                 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)
748                 if interactive and question( msg ):
749                     self.authorities[signing_address] = auth_name
750                 else:
751                     target = None
752
753         if target:
754             self.aliases[alias] = (signing_address, target)
755             
756         return target
757
758
759     def parse_url(self, url, show_message, question):
760         o = url[8:].split('?')
761         address = o[0]
762         if len(o)>1:
763             params = o[1].split('&')
764         else:
765             params = []
766
767         amount = label = message = signature = identity = ''
768         for p in params:
769             k,v = p.split('=')
770             uv = urldecode(v)
771             if k == 'amount': amount = uv
772             elif k == 'message': message = uv
773             elif k == 'label': label = uv
774             elif k == 'signature':
775                 identity, signature = uv.split(':')
776                 url = url.replace('&%s=%s'%(k,v),'')
777             else: 
778                 print k,v
779
780         if label and self.labels.get(address) != label:
781             if question('Give label "%s" to address %s ?'%(label,address)):
782                 if address not in self.addressbook and address not in self.all_addresses(): 
783                     self.addressbook.append(address)
784                 self.labels[address] = label
785
786         if signature:
787             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity):
788                 signing_address = self.get_alias(identity, True, show_message, question)
789             elif self.is_valid(identity):
790                 signing_address = identity
791             else:
792                 signing_address = None
793             if not signing_address:
794                 return
795             try:
796                 self.verify_message(signing_address, signature, url )
797                 self.receipt = (signing_address, signature, url)
798             except:
799                 show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.')
800                 address = amount = label = identity = message = ''
801
802         if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address):
803             payto_address = self.get_alias(address, True, show_message, question)
804             if payto_address:
805                 address = address + ' <' + payto_address + '>'
806
807         return address, amount, label, message, signature, identity, url
808
809
810     def update(self):
811         self.interface.poke('synchronizer')
812         self.up_to_date_event.wait(10000000000)
813
814
815     def freeze(self,addr):
816         if addr in self.all_addresses() and addr not in self.frozen_addresses:
817             self.unprioritize(addr)
818             self.frozen_addresses.append(addr)
819             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
820             return True
821         else:
822             return False
823
824     def unfreeze(self,addr):
825         if addr in self.all_addresses() and addr in self.frozen_addresses:
826             self.frozen_addresses.remove(addr)
827             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
828             return True
829         else:
830             return False
831
832     def prioritize(self,addr):
833         if addr in self.all_addresses() and addr not in self.prioritized_addresses:
834             self.unfreeze(addr)
835             self.prioritized_addresses.append(addr)
836             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
837             return True
838         else:
839             return False
840
841     def unprioritize(self,addr):
842         if addr in self.all_addresses() and addr in self.prioritized_addresses:
843             self.prioritized_addresses.remove(addr)
844             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
845             return True
846         else:
847             return False
848
849     def save(self):
850         s = {
851             'seed_version': self.seed_version,
852             'use_encryption': self.use_encryption,
853             'use_change': self.use_change,
854             'master_public_key': self.master_public_key,
855             'fee': self.fee,
856             'seed': self.seed,
857             'addresses': self.addresses,
858             'change_addresses': self.change_addresses,
859             'addr_history': self.history, 
860             'labels': self.labels,
861             'contacts': self.addressbook,
862             'imported_keys': self.imported_keys,
863             'aliases': self.aliases,
864             'authorities': self.authorities,
865             'receipts': self.receipts,
866             'num_zeros': self.num_zeros,
867             'frozen_addresses': self.frozen_addresses,
868             'prioritized_addresses': self.prioritized_addresses,
869             'gap_limit': self.gap_limit,
870             'transactions': self.transactions,
871         }
872         for k, v in s.items():
873             self.config.set_key(k,v)
874         self.config.save()
875
876     def set_verifier(self, verifier):
877         self.verifier = verifier
878             
879         # set the timestamp for transactions that need it
880         for hist in self.history.values():
881             for tx_hash, tx_height in hist:
882                 tx = self.transactions.get(tx_hash)
883                 if tx and not tx.get('timestamp'):
884                     timestamp = self.verifier.get_timestamp(tx_height)
885                     if timestamp:
886                         self.set_tx_timestamp(tx_hash, timestamp)
887
888                 if tx_height>0:
889                     self.verifier.add(tx_hash)
890
891
892
893     def set_tx_timestamp(self, tx_hash, timestamp):
894         with self.lock:
895             self.transactions[tx_hash]['timestamp'] = timestamp
896
897
898
899
900
901 class WalletSynchronizer(threading.Thread):
902
903
904     def __init__(self, wallet, config):
905         threading.Thread.__init__(self)
906         self.daemon = True
907         self.wallet = wallet
908         self.interface = self.wallet.interface
909         self.interface.register_channel('synchronizer')
910         self.wallet.interface.register_callback('connected', self.wallet.init_up_to_date)
911         self.wallet.interface.register_callback('connected', lambda: self.interface.send([('server.banner',[])],'synchronizer') )
912         self.was_updated = True
913
914     def synchronize_wallet(self):
915         new_addresses = self.wallet.synchronize()
916         if new_addresses:
917             self.subscribe_to_addresses(new_addresses)
918             
919         if self.interface.is_up_to_date('synchronizer'):
920             if not self.wallet.up_to_date:
921                 self.wallet.up_to_date = True
922                 self.was_updated = True
923                 self.wallet.up_to_date_event.set()
924         else:
925             if self.wallet.up_to_date:
926                 self.wallet.up_to_date = False
927                 self.was_updated = True
928
929
930
931     def subscribe_to_addresses(self, addresses):
932         messages = []
933         for addr in addresses:
934             messages.append(('blockchain.address.subscribe', [addr]))
935         self.interface.send( messages, 'synchronizer')
936
937
938     def run(self):
939         requested_tx = []
940         missing_tx = []
941         requested_histories = {}
942
943         # request any missing transactions
944         for history in self.wallet.history.values():
945             for tx_hash, tx_height in history:
946                 if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
947                     missing_tx.append( (tx_hash, tx_height) )
948         print_error("missing tx", missing_tx)
949
950         # wait until we are connected, in case the user is not connected
951         while not self.interface.is_connected:
952             time.sleep(1)
953         
954         # request banner, because 'connected' event happens before this thread is started
955         self.interface.send([('server.banner',[])],'synchronizer')
956
957         # subscriptions
958         self.subscribe_to_addresses(self.wallet.all_addresses())
959
960         while True:
961             # 1. send new requests
962             self.synchronize_wallet()
963
964             for tx_hash, tx_height in missing_tx:
965                 if (tx_hash, tx_height) not in requested_tx:
966                     self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
967                     requested_tx.append( (tx_hash, tx_height) )
968             missing_tx = []
969
970             if self.was_updated:
971                 self.interface.trigger_callback('updated')
972                 self.was_updated = False
973
974             # 2. get a response
975             r = self.interface.get_response('synchronizer')
976             if not r: 
977                 continue
978
979             # 3. handle response
980             method = r['method']
981             params = r['params']
982             result = r['result']
983
984             if method == 'blockchain.address.subscribe':
985                 addr = params[0]
986                 if self.wallet.get_status(self.wallet.get_history(addr)) != result:
987                     self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
988                     requested_histories[addr] = result
989
990             elif method == 'blockchain.address.get_history':
991                 addr = params[0]
992                 hist = []
993
994                 # check that txids are unique
995                 txids = []
996                 for item in result:
997                     tx_hash = item['tx_hash']
998                     if tx_hash not in txids:
999                         txids.append(tx_hash)
1000                         hist.append( (tx_hash, item['height']) )
1001
1002                 if len(hist) != len(result):
1003                     print "error: non-unique txid"
1004                     continue
1005
1006                 # check that the status corresponds to what was announced
1007                 if self.wallet.get_status(hist) != requested_histories.pop(addr):
1008                     print "error: status mismatch:", addr
1009                     continue
1010                 
1011                 # store received history
1012                 self.wallet.receive_history_callback(addr, hist)
1013
1014                 # request transactions that we don't have 
1015                 for tx_hash, tx_height in hist:
1016                     if self.wallet.transactions.get(tx_hash) is None:
1017                         if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
1018                             missing_tx.append( (tx_hash, tx_height) )
1019                     else:
1020                         timestamp = self.wallet.verifier.get_timestamp(tx_height)
1021                         self.wallet.set_tx_timestamp(tx_hash, timestamp)
1022
1023             elif method == 'blockchain.transaction.get':
1024                 tx_hash = params[0]
1025                 tx_height = params[1]
1026                 d = self.deserialize_tx(tx_hash, tx_height, result)
1027                 self.wallet.receive_tx_callback(tx_hash, d)
1028                 self.was_updated = True
1029                 requested_tx.remove( (tx_hash, tx_height) )
1030                 print_error("received tx:", d)
1031
1032             elif method == 'blockchain.transaction.broadcast':
1033                 self.wallet.tx_result = result
1034                 self.wallet.tx_event.set()
1035
1036             elif method == 'server.banner':
1037                 self.wallet.banner = result
1038                 self.was_updated = True
1039
1040             else:
1041                 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
1042
1043             if self.was_updated and not requested_tx:
1044                 self.interface.trigger_callback('updated')
1045                 self.was_updated = False
1046
1047
1048     def deserialize_tx(self, tx_hash, tx_height, raw_tx):
1049
1050         assert tx_hash == hash_encode(Hash(raw_tx.decode('hex')))
1051         import deserialize
1052         vds = deserialize.BCDataStream()
1053         vds.write(raw_tx.decode('hex'))
1054         d = deserialize.parse_Transaction(vds)
1055         d['tx_hash'] = tx_hash
1056         d['timestamp'] = self.wallet.verifier.get_timestamp(tx_height)
1057         return d
1058