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