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