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