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