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