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