wallet.get_label method
[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_status(self, address):
529         with self.lock:
530             h = self.history.get(address)
531         if not h: return None
532         status = ''
533         for tx_hash, height in h:
534             status += tx_hash + ':%d:' % height
535         return hashlib.sha256( status ).digest().encode('hex')
536
537
538
539     def receive_tx_callback(self, tx_hash, d):
540         #print "updating history for", addr
541         #with self.lock:
542         self.transactions[tx_hash] = d
543         self.update_tx_outputs(tx_hash)
544
545         if self.verifier: self.verifier.add(tx_hash)
546         self.save()
547
548
549     def receive_history_callback(self, addr, hist):
550         #print "updating history for", addr
551         with self.lock:
552             self.history[addr] = hist
553             self.save()
554
555
556
557     def get_tx_history(self):
558         with self.lock:
559             lines = self.transactions.values()
560
561         lines = sorted(lines, key=operator.itemgetter("timestamp"))
562         return lines
563
564     def get_transactions_at_height(self, height):
565         with self.lock:
566             values = self.transactions.values()[:]
567
568         out = []
569         for tx in values:
570             if tx['height'] == height:
571                 out.append(tx['tx_hash'])
572         return out
573
574
575     def get_label(self, tx_hash):
576         label = self.labels.get(tx_hash)
577         is_default = (label == '') or (label is None)
578         if is_default: label = self.get_default_label(tx_hash)
579         return label, is_default
580
581     def get_default_label(self, tx_hash):
582         tx = self.transactions.get(tx_hash)
583         if tx:
584             default_label = ''
585             if self.get_tx_value(tx_hash)<0:
586                 for o in tx['outputs']:
587                     o_addr = o.get('address')
588                     if not self.is_mine(o_addr):
589                         try:
590                             default_label = self.labels[o_addr]
591                         except KeyError:
592                             default_label = o_addr
593             else:
594                 for o in tx['outputs']:
595                     o_addr = o.get('address')
596                     if self.is_mine(o_addr) and not self.is_change(o_addr):
597                         break
598                 else:
599                     for o in tx['outputs']:
600                         o_addr = o.get('address')
601                         if self.is_mine(o_addr):
602                             break
603                     else:
604                         o_addr = None
605
606                 if o_addr:
607                     dest_label = self.labels.get(o_addr)
608                     try:
609                         default_label = self.labels[o_addr]
610                     except KeyError:
611                         default_label = o_addr
612
613         return default_label
614
615
616     def mktx(self, to_address, amount, label, password, fee=None, change_addr=None, from_addr= None):
617         if not self.is_valid(to_address):
618             raise ValueError("Invalid address")
619         inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr )
620         if not inputs:
621             raise ValueError("Not enough funds")
622
623         if not self.use_change and not change_addr:
624             change_addr = inputs[0][0]
625             print "Sending change to", change_addr
626
627         outputs = self.choose_tx_outputs( to_address, amount, fee, total, change_addr )
628         s_inputs = self.sign_inputs( inputs, outputs, password )
629
630         tx = filter( raw_tx( s_inputs, outputs ) )
631         if to_address not in self.addressbook:
632             self.addressbook.append(to_address)
633         if label: 
634             tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
635             self.labels[tx_hash] = label
636
637         return tx
638
639     def sendtx(self, tx):
640         # synchronous
641         h = self.send_tx(tx)
642         self.tx_event.wait()
643         self.receive_tx(h)
644
645     def send_tx(self, tx):
646         # asynchronous
647         self.tx_event.clear()
648         tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
649         self.interface.send([('blockchain.transaction.broadcast', [tx])], 'synchronizer')
650         return tx_hash
651
652     def receive_tx(self,tx_hash):
653         out = self.tx_result 
654         if out != tx_hash:
655             return False, "error: " + out
656         if self.receipt:
657             self.receipts[tx_hash] = self.receipt
658             self.receipt = None
659         return True, out
660
661
662     def read_alias(self, alias):
663         # this might not be the right place for this function.
664         import urllib
665
666         m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias)
667         m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias)
668         if m1:
669             url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) 
670         elif m2:
671             url = 'https://' + alias + '/bitcoin.id'
672         else:
673             return ''
674         try:
675             lines = urllib.urlopen(url).readlines()
676         except:
677             return ''
678
679         # line 0
680         line = lines[0].strip().split(':')
681         if len(line) == 1:
682             auth_name = None
683             target = signing_addr = line[0]
684         else:
685             target, auth_name, signing_addr, signature = line
686             msg = "alias:%s:%s:%s"%(alias,target,auth_name)
687             print msg, signature
688             self.verify_message(signing_addr, signature, msg)
689         
690         # other lines are signed updates
691         for line in lines[1:]:
692             line = line.strip()
693             if not line: continue
694             line = line.split(':')
695             previous = target
696             print repr(line)
697             target, signature = line
698             self.verify_message(previous, signature, "alias:%s:%s"%(alias,target))
699
700         if not self.is_valid(target):
701             raise ValueError("Invalid bitcoin address")
702
703         return target, signing_addr, auth_name
704
705     def update_password(self, seed, old_password, new_password):
706         if new_password == '': new_password = None
707         self.use_encryption = (new_password != None)
708         self.seed = self.pw_encode( seed, new_password)
709         for k in self.imported_keys.keys():
710             a = self.imported_keys[k]
711             b = self.pw_decode(a, old_password)
712             c = self.pw_encode(b, new_password)
713             self.imported_keys[k] = c
714         self.save()
715
716     def get_alias(self, alias, interactive = False, show_message=None, question = None):
717         try:
718             target, signing_address, auth_name = self.read_alias(alias)
719         except BaseException, e:
720             # raise exception if verify fails (verify the chain)
721             if interactive:
722                 show_message("Alias error: " + str(e))
723             return
724
725         print target, signing_address, auth_name
726
727         if auth_name is None:
728             a = self.aliases.get(alias)
729             if not a:
730                 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)
731                 if interactive and question( msg ):
732                     self.aliases[alias] = (signing_address, target)
733                 else:
734                     target = None
735             else:
736                 if signing_address != a[0]:
737                     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
738                     if interactive and question( msg ):
739                         self.aliases[alias] = (signing_address, target)
740                     else:
741                         target = None
742         else:
743             if signing_address not in self.authorities.keys():
744                 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)
745                 if interactive and question( msg ):
746                     self.authorities[signing_address] = auth_name
747                 else:
748                     target = None
749
750         if target:
751             self.aliases[alias] = (signing_address, target)
752             
753         return target
754
755
756     def parse_url(self, url, show_message, question):
757         o = url[8:].split('?')
758         address = o[0]
759         if len(o)>1:
760             params = o[1].split('&')
761         else:
762             params = []
763
764         amount = label = message = signature = identity = ''
765         for p in params:
766             k,v = p.split('=')
767             uv = urldecode(v)
768             if k == 'amount': amount = uv
769             elif k == 'message': message = uv
770             elif k == 'label': label = uv
771             elif k == 'signature':
772                 identity, signature = uv.split(':')
773                 url = url.replace('&%s=%s'%(k,v),'')
774             else: 
775                 print k,v
776
777         if label and self.labels.get(address) != label:
778             if question('Give label "%s" to address %s ?'%(label,address)):
779                 if address not in self.addressbook and address not in self.all_addresses(): 
780                     self.addressbook.append(address)
781                 self.labels[address] = label
782
783         if signature:
784             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity):
785                 signing_address = self.get_alias(identity, True, show_message, question)
786             elif self.is_valid(identity):
787                 signing_address = identity
788             else:
789                 signing_address = None
790             if not signing_address:
791                 return
792             try:
793                 self.verify_message(signing_address, signature, url )
794                 self.receipt = (signing_address, signature, url)
795             except:
796                 show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.')
797                 address = amount = label = identity = message = ''
798
799         if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address):
800             payto_address = self.get_alias(address, True, show_message, question)
801             if payto_address:
802                 address = address + ' <' + payto_address + '>'
803
804         return address, amount, label, message, signature, identity, url
805
806
807     def update(self):
808         self.interface.poke('synchronizer')
809         self.up_to_date_event.wait(10000000000)
810
811
812     def freeze(self,addr):
813         if addr in self.all_addresses() and addr not in self.frozen_addresses:
814             self.unprioritize(addr)
815             self.frozen_addresses.append(addr)
816             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
817             return True
818         else:
819             return False
820
821     def unfreeze(self,addr):
822         if addr in self.all_addresses() and addr in self.frozen_addresses:
823             self.frozen_addresses.remove(addr)
824             self.config.set_key('frozen_addresses', self.frozen_addresses, True)
825             return True
826         else:
827             return False
828
829     def prioritize(self,addr):
830         if addr in self.all_addresses() and addr not in self.prioritized_addresses:
831             self.unfreeze(addr)
832             self.prioritized_addresses.append(addr)
833             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
834             return True
835         else:
836             return False
837
838     def unprioritize(self,addr):
839         if addr in self.all_addresses() and addr in self.prioritized_addresses:
840             self.prioritized_addresses.remove(addr)
841             self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
842             return True
843         else:
844             return False
845
846     def save(self):
847         s = {
848             'seed_version': self.seed_version,
849             'use_encryption': self.use_encryption,
850             'use_change': self.use_change,
851             'master_public_key': self.master_public_key,
852             'fee': self.fee,
853             'seed': self.seed,
854             'addresses': self.addresses,
855             'change_addresses': self.change_addresses,
856             'addr_history': self.history, 
857             'labels': self.labels,
858             'contacts': self.addressbook,
859             'imported_keys': self.imported_keys,
860             'aliases': self.aliases,
861             'authorities': self.authorities,
862             'receipts': self.receipts,
863             'num_zeros': self.num_zeros,
864             'frozen_addresses': self.frozen_addresses,
865             'prioritized_addresses': self.prioritized_addresses,
866             'gap_limit': self.gap_limit,
867             'transactions': self.transactions,
868         }
869         for k, v in s.items():
870             self.config.set_key(k,v)
871         self.config.save()
872
873     def set_verifier(self, verifier):
874         self.verifier = verifier
875         for tx_hash in self.transactions.keys(): 
876             self.verifier.add(tx_hash)
877             
878         # set the timestamp for transactions that need it
879         for l in self.history.values():
880             for tx_hash, tx_height in l:
881                 tx = self.transactions.get(tx_hash)
882                 if tx and not tx.get('timestamp'):
883                     timestamp = self.verifier.get_timestamp(tx_height)
884                     if timestamp:
885                         self.set_tx_timestamp(tx_hash, timestamp)
886
887
888
889
890     def set_tx_timestamp(self, tx_hash, timestamp):
891         with self.lock:
892             self.transactions[tx_hash]['timestamp'] = timestamp
893
894
895
896
897
898 class WalletSynchronizer(threading.Thread):
899
900
901     def __init__(self, wallet, config):
902         threading.Thread.__init__(self)
903         self.daemon = True
904         self.wallet = wallet
905         self.interface = self.wallet.interface
906         self.interface.register_channel('synchronizer')
907         self.wallet.interface.register_callback('connected', self.wallet.init_up_to_date)
908         self.wallet.interface.register_callback('connected', lambda: self.interface.send([('server.banner',[])],'synchronizer') )
909         self.was_updated = True
910
911     def synchronize_wallet(self):
912         new_addresses = self.wallet.synchronize()
913         if new_addresses:
914             self.subscribe_to_addresses(new_addresses)
915             
916         if self.interface.is_up_to_date('synchronizer'):
917             if not self.wallet.up_to_date:
918                 self.wallet.up_to_date = True
919                 self.was_updated = True
920                 self.wallet.up_to_date_event.set()
921         else:
922             if self.wallet.up_to_date:
923                 self.wallet.up_to_date = False
924                 self.was_updated = True
925
926
927
928     def subscribe_to_addresses(self, addresses):
929         messages = []
930         for addr in addresses:
931             messages.append(('blockchain.address.subscribe', [addr]))
932         self.interface.send( messages, 'synchronizer')
933
934
935     def run(self):
936         requested_tx = []
937         missing_tx = []
938
939         # request any missing transactions
940         for history in self.wallet.history.values():
941             for tx_hash, tx_height in history:
942                 if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
943                     missing_tx.append( (tx_hash, tx_height) )
944         print_error("missing tx", missing_tx)
945
946         # wait until we are connected, in case the user is not connected
947         while not self.interface.is_connected:
948             time.sleep(1)
949         
950         # request banner, because 'connected' event happens before this thread is started
951         self.interface.send([('server.banner',[])],'synchronizer')
952
953         # subscriptions
954         self.subscribe_to_addresses(self.wallet.all_addresses())
955
956         while True:
957             # 1. send new requests
958             self.synchronize_wallet()
959
960             for tx_hash, tx_height in missing_tx:
961                 if (tx_hash, tx_height) not in requested_tx:
962                     self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
963                     requested_tx.append( (tx_hash, tx_height) )
964             missing_tx = []
965
966             if self.was_updated:
967                 self.interface.trigger_callback('updated')
968                 self.was_updated = False
969
970             # 2. get a response
971             r = self.interface.get_response('synchronizer')
972             if not r: 
973                 continue
974
975             # 3. handle response
976             method = r['method']
977             params = r['params']
978             result = r['result']
979
980             if method == 'blockchain.address.subscribe':
981                 addr = params[0]
982                 if self.wallet.get_status(addr) != result:
983                     self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
984                             
985             elif method == 'blockchain.address.get_history':
986                 addr = params[0]
987                 hist = []
988                 # in the new protocol, we will receive a list of (tx_hash, height)
989                 for item in result: hist.append( (item['tx_hash'], item['height']) )
990                 # store it
991                 self.wallet.receive_history_callback(addr, hist)
992                 # request transactions that we don't have 
993                 for tx_hash, tx_height in hist:
994                     if self.wallet.transactions.get(tx_hash) is None:
995                         if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
996                             missing_tx.append( (tx_hash, tx_height) )
997                     else:
998                         timestamp = self.wallet.verifier.get_timestamp(tx_height)
999                         self.wallet.set_tx_timestamp(tx_hash, timestamp)
1000
1001             elif method == 'blockchain.transaction.get':
1002                 tx_hash = params[0]
1003                 tx_height = params[1]
1004                 d = self.deserialize_tx(tx_hash, tx_height, result)
1005                 self.wallet.receive_tx_callback(tx_hash, d)
1006                 self.was_updated = True
1007                 requested_tx.remove( (tx_hash, tx_height) )
1008                 print_error("received tx:", d)
1009
1010             elif method == 'blockchain.transaction.broadcast':
1011                 self.wallet.tx_result = result
1012                 self.wallet.tx_event.set()
1013
1014             elif method == 'server.banner':
1015                 self.wallet.banner = result
1016                 self.was_updated = True
1017
1018             else:
1019                 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
1020
1021             if self.was_updated and not requested_tx:
1022                 self.interface.trigger_callback('updated')
1023                 self.was_updated = False
1024
1025
1026     def deserialize_tx(self, tx_hash, tx_height, raw_tx):
1027
1028         assert tx_hash == hash_encode(Hash(raw_tx.decode('hex')))
1029         import deserialize
1030         vds = deserialize.BCDataStream()
1031         vds.write(raw_tx.decode('hex'))
1032         d = deserialize.parse_Transaction(vds)
1033         d['tx_hash'] = tx_hash
1034         d['timestamp'] = self.wallet.verifier.get_timestamp(tx_height)
1035         return d
1036