3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
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.
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.
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/>.
34 from ecdsa.util import string_to_number, number_to_string
35 from util import print_error, user_dir, format_satoshis
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)
43 EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s))
44 DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e))
47 from version import ELECTRUM_VERSION, SEED_VERSION
51 def __init__(self, config={}):
54 self.electrum_version = ELECTRUM_VERSION
55 self.gap_limit_for_change = 3 # constant
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
79 self.requested_amounts = config.get('requested_amounts',{}) # txid -> deserialised
82 self.prevout_values = {} # my own transaction outputs
83 self.spent_outputs = []
84 self.receipt = None # next receipt
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)
94 self.up_to_date = False
95 self.lock = threading.Lock()
96 self.tx_event = threading.Event()
98 if self.seed_version != SEED_VERSION:
99 raise ValueError("This wallet seed is deprecated. Please run upgrade.py for a diagnostic.")
101 for tx_hash in self.transactions.keys():
102 self.update_tx_outputs(tx_hash)
105 def set_up_to_date(self,b):
106 with self.lock: self.up_to_date = b
108 def is_up_to_date(self):
109 with self.lock: return self.up_to_date
112 self.up_to_date = False
113 self.interface.poke('synchronizer')
114 while not self.is_up_to_date(): time.sleep(0.1)
116 def import_key(self, sec, password):
119 seed = self.decode_seed(password)
121 raise BaseException("Invalid password")
123 # rebuild public key from private key, compressed or uncompressed
124 pkey = regenerate_key(sec)
128 # figure out if private key is compressed
129 compressed = is_compressed(sec)
131 # rebuild private and public key from regenerated secret
132 private_key = GetPrivKey(pkey, compressed)
133 public_key = GetPubKey(pkey.pubkey, compressed)
134 address = public_key_to_bc_address(public_key)
136 if address in self.all_addresses():
137 raise BaseException('Address already in wallet')
139 # store the originally requested keypair into the imported keys table
140 self.imported_keys[address] = self.pw_encode(sec, password )
144 def init_seed(self, seed):
145 if self.seed: raise BaseException("a seed exists")
147 seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
149 self.config.set_key('seed', self.seed, True)
150 self.config.set_key('seed_version', self.seed_version, True)
151 self.init_mpk(self.seed)
153 def init_mpk(self,seed):
156 secexp = self.stretch_key(seed)
157 master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
158 self.master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
159 self.config.set_key('master_public_key', self.master_public_key, True)
161 def all_addresses(self):
162 return self.addresses + self.change_addresses + self.imported_keys.keys()
164 def is_mine(self, address):
165 return address in self.all_addresses()
167 def is_change(self, address):
168 return address in self.change_addresses
170 def is_valid(self,addr):
171 ADDRESS_RE = re.compile('[1-9A-HJ-NP-Za-km-z]{26,}\\Z')
172 if not ADDRESS_RE.match(addr): return False
174 addrtype, h = bc_address_to_hash_160(addr)
177 return addr == hash_160_to_bc_address(h, addrtype)
179 def stretch_key(self,seed):
181 for i in range(100000):
182 seed = hashlib.sha256(seed + oldseed).digest()
183 return string_to_number( seed )
185 def get_sequence(self,n,for_change):
186 return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key.decode('hex') ) )
188 def get_private_key_base58(self, address, password):
189 secexp, compressed = self.get_private_key(address, password)
190 if secexp is None: return None
191 pk = number_to_string( secexp, generator_secp256k1.order() )
192 return SecretToASecret( pk, compressed )
194 def get_private_key(self, address, password):
195 """ Privatekey(type,n) = Master_private_key + H(n|S|type) """
196 order = generator_secp256k1.order()
198 if address in self.imported_keys.keys():
199 sec = self.pw_decode( self.imported_keys[address], password )
200 if not sec: return None, None
201 pkey = regenerate_key(sec)
202 compressed = is_compressed(sec)
206 if address in self.addresses:
207 n = self.addresses.index(address)
209 elif address in self.change_addresses:
210 n = self.change_addresses.index(address)
213 raise BaseException("unknown address", address)
215 seed = self.pw_decode( self.seed, password)
216 if not seed: return None
217 secexp = self.stretch_key(seed)
218 secexp = ( secexp + self.get_sequence(n,for_change) ) % order
220 pkey = EC_KEY(secexp)
222 public_key = GetPubKey(pkey.pubkey, compressed)
223 addr = public_key_to_bc_address(public_key)
225 print_error('Invalid password with correct decoding')
226 raise BaseException('Invalid password')
228 return secexp, compressed
230 def msg_magic(self, message):
231 return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message
233 def sign_message(self, address, message, password):
234 secexp, compressed = self.get_private_key(address, password)
235 private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
236 public_key = private_key.get_verifying_key()
237 signature = private_key.sign_digest( Hash( self.msg_magic( message ) ), sigencode = ecdsa.util.sigencode_string )
238 assert public_key.verify_digest( signature, Hash( self.msg_magic( message ) ), sigdecode = ecdsa.util.sigdecode_string)
240 sig = base64.b64encode( chr(27 + i + (4 if compressed else 0)) + signature )
242 self.verify_message( address, sig, message)
247 raise BaseException("error: cannot sign message")
250 def verify_message(self, address, signature, message):
251 """ See http://www.secg.org/download/aid-780/sec1-v2.pdf for the math """
252 from ecdsa import numbertheory, ellipticcurve, util
254 curve = curve_secp256k1
255 G = generator_secp256k1
257 # extract r,s from signature
258 sig = base64.b64decode(signature)
259 if len(sig) != 65: raise BaseException("Wrong encoding")
260 r,s = util.sigdecode_string(sig[1:], order)
262 if nV < 27 or nV >= 35:
263 raise BaseException("Bad encoding")
272 x = r + (recid/2) * order
274 alpha = ( x * x * x + curve.a() * x + curve.b() ) % curve.p()
275 beta = msqr.modular_sqrt(alpha, curve.p())
276 y = beta if (beta - recid) % 2 == 0 else curve.p() - beta
277 # 1.4 the constructor checks that nR is at infinity
278 R = ellipticcurve.Point(curve, x, y, order)
279 # 1.5 compute e from message:
280 h = Hash( self.msg_magic( message ) )
281 e = string_to_number(h)
283 # 1.6 compute Q = r^-1 (sR - eG)
284 inv_r = numbertheory.inverse_mod(r,order)
285 Q = inv_r * ( s * R + minus_e * G )
286 public_key = ecdsa.VerifyingKey.from_public_point( Q, curve = SECP256k1 )
287 # check that Q is the public key
288 public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string)
289 # check that we get the original signing address
290 addr = public_key_to_bc_address( encode_point(public_key, compressed) )
292 raise BaseException("Bad signature")
295 def create_new_address(self, for_change):
296 n = len(self.change_addresses) if for_change else len(self.addresses)
297 address = self.get_new_address(n, for_change)
299 self.change_addresses.append(address)
301 self.addresses.append(address)
302 self.history[address] = []
305 def get_new_address(self, n, for_change):
306 """ Publickey(type,n) = Master_public_key + H(n|S|type)*point """
308 z = self.get_sequence(n, for_change)
309 master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key.decode('hex'), curve = SECP256k1 )
310 pubkey_point = master_public_key.pubkey.point + z*curve.generator
311 public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 )
312 address = public_key_to_bc_address( '04'.decode('hex') + public_key2.to_string() )
317 def change_gap_limit(self, value):
318 if value >= self.gap_limit:
319 self.gap_limit = value
321 self.interface.poke('synchronizer')
324 elif value >= self.min_acceptable_gap():
325 k = self.num_unused_trailing_addresses()
326 n = len(self.addresses) - k + value
327 self.addresses = self.addresses[0:n]
328 self.gap_limit = value
334 def num_unused_trailing_addresses(self):
336 for a in self.addresses[::-1]:
337 if self.history.get(a):break
341 def min_acceptable_gap(self):
342 # fixme: this assumes wallet is synchronized
345 k = self.num_unused_trailing_addresses()
346 for a in self.addresses[0:-k]:
347 if self.history.get(a):
351 if n > nmax: nmax = n
355 def address_is_old(self, address):
357 h = self.history.get(address, [])
360 for tx_hash, tx_height in h:
364 tx_age = self.verifier.height - tx_height + 1
370 def synchronize_sequence(self, addresses, n, for_change):
373 if len(self.addresses) < n:
374 new_addresses.append( self.create_new_address(for_change) )
376 if map( lambda a: self.address_is_old(a), addresses[-n:] ) == n*[False]:
379 new_addresses.append( self.create_new_address(for_change) )
383 def synchronize(self):
384 if not self.master_public_key:
387 new_addresses += self.synchronize_sequence(self.addresses, self.gap_limit, False)
388 new_addresses += self.synchronize_sequence(self.change_addresses, self.gap_limit_for_change, True)
393 return (len(self.change_addresses) > self.gap_limit_for_change ) or ( len(self.addresses) > self.gap_limit )
395 def fill_addressbook(self):
396 for tx_hash, tx in self.transactions.items():
397 is_send, _, _ = self.get_tx_value(tx_hash)
399 for o in tx['outputs']:
400 addr = o.get('address')
401 if not self.is_mine(addr) and addr not in self.addressbook:
402 self.addressbook.append(addr)
404 # self.update_tx_labels()
407 def get_address_flags(self, addr):
408 flags = "C" if self.is_change(addr) else "I" if addr in self.imported_keys.keys() else "-"
409 flags += "F" if addr in self.frozen_addresses else "P" if addr in self.prioritized_addresses else "-"
413 def get_tx_value(self, tx_hash, addresses = None):
414 # return the balance for that tx
415 if addresses is None: addresses = self.all_addresses()
419 v_in = v_out = v_out_mine = 0
420 d = self.transactions.get(tx_hash)
424 for item in d.get('inputs'):
425 addr = item.get('address')
426 if addr in addresses:
428 key = item['prevout_hash'] + ':%d'%item['prevout_n']
429 value = self.prevout_values.get( key )
437 for item in d.get('outputs'):
438 addr = item.get('address')
439 value = item.get('value')
441 if addr in addresses:
445 # all inputs are mine:
447 v = v_out_mine - v_in
449 # some inputs are mine:
452 v = v_out_mine - v_out
457 return is_send, v, fee
460 def get_tx_details(self, tx_hash):
462 if not tx_hash: return ''
463 tx = self.transactions.get(tx_hash)
464 is_mine, v, fee = self.get_tx_value(tx_hash)
465 conf, timestamp = self.verifier.get_confirmations(tx_hash)
468 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
472 inputs = map(lambda x: x.get('address'), tx['inputs'])
473 outputs = map(lambda x: x.get('address'), tx['outputs'])
474 tx_details = "Transaction Details" +"\n\n" \
475 + "Transaction ID:\n" + tx_hash + "\n\n" \
476 + "Status: %d confirmations\n"%conf
479 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
480 + "Transaction fee: %s\n"% format_satoshis(fee, False)
482 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
483 + "Transaction fee: unknown\n"
485 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
487 tx_details += "Date: %s\n\n"%time_str \
488 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
489 + "Outputs:\n-"+ '\n-'.join(outputs)
491 r = self.receipts.get(tx_hash)
493 tx_details += "\n_______________________________________" \
494 + '\n\nSigned URI: ' + r[2] \
495 + "\n\nSigned by: " + r[0] \
496 + '\n\nSignature: ' + r[1]
501 def update_tx_outputs(self, tx_hash):
502 tx = self.transactions.get(tx_hash)
503 for item in tx.get('outputs'):
504 value = item.get('value')
505 key = tx_hash+ ':%d'%item.get('index')
507 self.prevout_values[key] = value
509 for item in tx.get('inputs'):
510 if self.is_mine(item.get('address')):
511 key = item['prevout_hash'] + ':%d'%item['prevout_n']
512 self.spent_outputs.append(key)
515 def get_addr_balance(self, address):
516 assert self.is_mine(address)
517 h = self.history.get(address,[])
518 if h == ['*']: return 0,0
520 received_coins = [] # list of coins received at address
522 for tx_hash, tx_height in h:
523 d = self.transactions.get(tx_hash)
525 for item in d.get('outputs'):
526 addr = item.get('address')
528 key = tx_hash + ':%d'%item['index']
529 received_coins.append(key)
531 for tx_hash, tx_height in h:
532 d = self.transactions.get(tx_hash)
536 for item in d.get('inputs'):
537 addr = item.get('address')
539 key = item['prevout_hash'] + ':%d'%item['prevout_n']
540 value = self.prevout_values.get( key )
541 if key in received_coins:
544 for item in d.get('outputs'):
545 addr = item.get('address')
546 key = tx_hash + ':%d'%item['index']
548 v += item.get('value')
556 def get_balance(self):
558 for addr in self.all_addresses():
559 c, u = self.get_addr_balance(addr)
565 def choose_tx_inputs( self, amount, fixed_fee, from_addr = None ):
566 """ todo: minimize tx size """
568 fee = self.fee if fixed_fee is None else fixed_fee
571 prioritized_coins = []
572 domain = [from_addr] if from_addr else self.all_addresses()
573 for i in self.frozen_addresses:
574 if i in domain: domain.remove(i)
576 for i in self.prioritized_addresses:
577 if i in domain: domain.remove(i)
580 h = self.history.get(addr, [])
581 if h == ['*']: continue
582 for tx_hash, tx_height in h:
583 tx = self.transactions.get(tx_hash)
584 for output in tx.get('outputs'):
585 if output.get('address') != addr: continue
586 key = tx_hash + ":%d" % output.get('index')
587 if key in self.spent_outputs: continue
588 output['tx_hash'] = tx_hash
592 for addr in self.prioritized_addresses:
593 h = self.history.get(addr, [])
594 if h == ['*']: continue
595 for tx_hash, tx_height in h:
596 tx = self.transactions.get(tx_hash)
597 for output in tx.get('outputs'):
598 if output.get('address') != addr: continue
599 key = tx_hash + ":%d" % output.get('index')
600 if key in self.spent_outputs: continue
601 output['tx_hash'] = tx_hash
602 prioritized_coins.append(output)
606 coins = prioritized_coins + coins
609 addr = item.get('address')
610 v = item.get('value')
612 item['pubkeysig'] = [(None, None)]
613 inputs.append( item )
614 fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee
615 if total >= amount + fee: break
617 #print "not enough funds: %s %s"%(format_satoshis(total), format_satoshis(fee))
619 return inputs, total, fee
621 def add_tx_change( self, outputs, amount, fee, total, change_addr=None ):
622 change_amount = total - ( amount + fee )
623 if change_amount != 0:
624 # normally, the update thread should ensure that the last change address is unused
626 change_addr = self.change_addresses[-self.gap_limit_for_change]
627 # Insert the change output at a random position in the outputs
628 posn = random.randint(0, len(outputs))
629 outputs[posn:posn] = [( change_addr, change_amount)]
633 def pw_encode(self, s, password):
635 secret = Hash(password)
636 return EncodeAES(secret, s)
640 def pw_decode(self, s, password):
641 if password is not None:
642 secret = Hash(password)
644 d = DecodeAES(secret, s)
646 raise BaseException('Invalid password')
651 def decode_seed(self, password):
652 seed = self.pw_decode(self.seed, password)
654 # check decoded seed with master public key
656 secexp = self.stretch_key(seed)
657 master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
658 master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
659 if master_public_key != self.master_public_key:
660 print_error('invalid password (mpk)')
661 raise BaseException('Invalid password')
666 def get_history(self, address):
668 return self.history.get(address)
670 def get_status(self, h):
671 if not h: return None
672 if h == ['*']: return '*'
674 for tx_hash, height in h:
675 status += tx_hash + ':%d:' % height
676 return hashlib.sha256( status ).digest().encode('hex')
680 def receive_tx_callback(self, tx_hash, tx):
682 if not self.check_new_tx(tx_hash, tx):
683 raise BaseException("error: received transaction is not consistent with history"%tx_hash)
686 self.transactions[tx_hash] = tx
688 tx_height = tx.get('height')
689 if self.verifier and tx_height>0:
690 self.verifier.add(tx_hash, tx_height)
692 self.update_tx_outputs(tx_hash)
697 def receive_history_callback(self, addr, hist):
699 if not self.check_new_history(addr, hist):
700 raise BaseException("error: received history for %s is not consistent with known transactions"%addr)
703 self.history[addr] = hist
707 for tx_hash, tx_height in hist:
709 # add it in case it was previously unconfirmed
710 if self.verifier: self.verifier.add(tx_hash, tx_height)
711 # set the height in case it changed
712 tx = self.transactions.get(tx_hash)
714 if tx.get('height') != tx_height:
715 print_error( "changing height for tx", tx_hash )
716 tx['height'] = tx_height
719 def get_tx_history(self):
721 history = self.transactions.values()
722 history.sort(key = lambda x: x.get('height') if x.get('height') else 1e12)
727 is_mine, v, fee = self.get_tx_value(tx['tx_hash'])
728 if v is not None: balance += v
729 c, u = self.get_balance()
732 v_str = format_satoshis( c+u - balance, True, self.num_zeros)
733 result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
735 balance = c + u - balance
737 tx_hash = tx['tx_hash']
738 conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
739 is_mine, value, fee = self.get_tx_value(tx_hash)
740 if value is not None:
743 result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
747 def get_transactions_at_height(self, height):
749 values = self.transactions.values()[:]
753 if tx['height'] == height:
754 out.append(tx['tx_hash'])
758 def get_label(self, tx_hash):
759 label = self.labels.get(tx_hash)
760 is_default = (label == '') or (label is None)
761 if is_default: label = self.get_default_label(tx_hash)
762 return label, is_default
764 def get_default_label(self, tx_hash):
765 tx = self.transactions.get(tx_hash)
768 is_mine, _, _ = self.get_tx_value(tx_hash)
770 for o in tx['outputs']:
771 o_addr = o.get('address')
772 if not self.is_mine(o_addr):
774 default_label = self.labels[o_addr]
776 default_label = o_addr
778 for o in tx['outputs']:
779 o_addr = o.get('address')
780 if self.is_mine(o_addr) and not self.is_change(o_addr):
783 for o in tx['outputs']:
784 o_addr = o.get('address')
785 if self.is_mine(o_addr):
791 dest_label = self.labels.get(o_addr)
793 default_label = self.labels[o_addr]
795 default_label = o_addr
800 def mktx(self, outputs, label, password, fee=None, change_addr=None, from_addr= None):
802 for address, x in outputs:
803 assert self.is_valid(address)
805 amount = sum( map(lambda x:x[1], outputs) )
806 inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr )
808 raise ValueError("Not enough funds")
810 if not self.use_change and not change_addr:
811 change_addr = inputs[-1]['address']
812 print_error( "Sending change to", change_addr )
813 outputs = self.add_tx_change(outputs, amount, fee, total, change_addr)
816 return repr({'inputs':inputs, 'outputs':outputs})
818 tx = self.signed_tx(inputs, outputs, password)
820 for address, x in outputs:
821 if address not in self.addressbook and not self.is_mine(address):
822 self.addressbook.append(address)
825 tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
826 self.labels[tx_hash] = label
830 def signed_tx(self, inputs, outputs, password):
831 tx = Transaction.from_io(inputs, outputs)
833 for txin in tx.inputs:
834 addr = txin['address']
835 secexp, compressed = self.get_private_key(addr, password)
836 private_keys[addr] = (secexp,compressed)
837 tx.sign(private_keys)
840 def sendtx(self, tx):
844 return self.receive_tx(h)
846 def send_tx(self, tx):
848 self.tx_event.clear()
849 tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
850 self.interface.send([('blockchain.transaction.broadcast', [tx])], 'synchronizer')
853 def receive_tx(self,tx_hash):
856 return False, "error: " + out
858 self.receipts[tx_hash] = self.receipt
863 def read_alias(self, alias):
864 # this might not be the right place for this function.
867 m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias)
868 m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias)
870 url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1)
872 url = 'https://' + alias + '/bitcoin.id'
876 lines = urllib.urlopen(url).readlines()
881 line = lines[0].strip().split(':')
884 target = signing_addr = line[0]
886 target, auth_name, signing_addr, signature = line
887 msg = "alias:%s:%s:%s"%(alias,target,auth_name)
889 self.verify_message(signing_addr, signature, msg)
891 # other lines are signed updates
892 for line in lines[1:]:
894 if not line: continue
895 line = line.split(':')
898 target, signature = line
899 self.verify_message(previous, signature, "alias:%s:%s"%(alias,target))
901 if not self.is_valid(target):
902 raise ValueError("Invalid bitcoin address")
904 return target, signing_addr, auth_name
906 def update_password(self, seed, old_password, new_password):
907 if new_password == '': new_password = None
908 self.use_encryption = (new_password != None)
909 self.seed = self.pw_encode( seed, new_password)
910 self.config.set_key('seed', self.seed, True)
911 for k in self.imported_keys.keys():
912 a = self.imported_keys[k]
913 b = self.pw_decode(a, old_password)
914 c = self.pw_encode(b, new_password)
915 self.imported_keys[k] = c
918 def get_alias(self, alias, interactive = False, show_message=None, question = None):
920 target, signing_address, auth_name = self.read_alias(alias)
921 except BaseException, e:
922 # raise exception if verify fails (verify the chain)
924 show_message("Alias error: " + str(e))
927 print target, signing_address, auth_name
929 if auth_name is None:
930 a = self.aliases.get(alias)
932 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)
933 if interactive and question( msg ):
934 self.aliases[alias] = (signing_address, target)
938 if signing_address != a[0]:
939 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
940 if interactive and question( msg ):
941 self.aliases[alias] = (signing_address, target)
945 if signing_address not in self.authorities.keys():
946 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)
947 if interactive and question( msg ):
948 self.authorities[signing_address] = auth_name
953 self.aliases[alias] = (signing_address, target)
958 def parse_url(self, url, show_message, question):
959 o = url[8:].split('?')
962 params = o[1].split('&')
966 amount = label = message = signature = identity = ''
970 if k == 'amount': amount = uv
971 elif k == 'message': message = uv
972 elif k == 'label': label = uv
973 elif k == 'signature':
974 identity, signature = uv.split(':')
975 url = url.replace('&%s=%s'%(k,v),'')
979 if label and self.labels.get(address) != label:
980 if question('Give label "%s" to address %s ?'%(label,address)):
981 if address not in self.addressbook and address not in self.all_addresses():
982 self.addressbook.append(address)
983 self.labels[address] = label
986 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity):
987 signing_address = self.get_alias(identity, True, show_message, question)
988 elif self.is_valid(identity):
989 signing_address = identity
991 signing_address = None
992 if not signing_address:
995 self.verify_message(signing_address, signature, url )
996 self.receipt = (signing_address, signature, url)
998 show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.')
999 address = amount = label = identity = message = ''
1001 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address):
1002 payto_address = self.get_alias(address, True, show_message, question)
1004 address = address + ' <' + payto_address + '>'
1006 return address, amount, label, message, signature, identity, url
1010 def freeze(self,addr):
1011 if addr in self.all_addresses() and addr not in self.frozen_addresses:
1012 self.unprioritize(addr)
1013 self.frozen_addresses.append(addr)
1014 self.config.set_key('frozen_addresses', self.frozen_addresses, True)
1019 def unfreeze(self,addr):
1020 if addr in self.all_addresses() and addr in self.frozen_addresses:
1021 self.frozen_addresses.remove(addr)
1022 self.config.set_key('frozen_addresses', self.frozen_addresses, True)
1027 def prioritize(self,addr):
1028 if addr in self.all_addresses() and addr not in self.prioritized_addresses:
1030 self.prioritized_addresses.append(addr)
1031 self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
1036 def unprioritize(self,addr):
1037 if addr in self.all_addresses() and addr in self.prioritized_addresses:
1038 self.prioritized_addresses.remove(addr)
1039 self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
1046 'use_encryption': self.use_encryption,
1047 'use_change': self.use_change,
1049 'addresses': self.addresses,
1050 'change_addresses': self.change_addresses,
1051 'addr_history': self.history,
1052 'labels': self.labels,
1053 'contacts': self.addressbook,
1054 'imported_keys': self.imported_keys,
1055 'aliases': self.aliases,
1056 'authorities': self.authorities,
1057 'receipts': self.receipts,
1058 'num_zeros': self.num_zeros,
1059 'frozen_addresses': self.frozen_addresses,
1060 'prioritized_addresses': self.prioritized_addresses,
1061 'gap_limit': self.gap_limit,
1062 'transactions': self.transactions,
1063 'requested_amounts': self.requested_amounts,
1065 for k, v in s.items():
1066 self.config.set_key(k,v)
1069 def set_verifier(self, verifier):
1070 self.verifier = verifier
1072 # review stored transactions and send them to the verifier
1073 # (they are not necessarily in the history, because history items might have have been pruned)
1074 for tx_hash, tx in self.transactions.items():
1075 tx_height = tx.get('height')
1077 print_error( "skipping", tx_hash, tx_height )
1081 self.verifier.add(tx_hash, tx_height)
1083 # review transactions that are in the history
1084 for addr, hist in self.history.items():
1085 if hist == ['*']: continue
1086 for tx_hash, tx_height in hist:
1088 # add it in case it was previously unconfirmed
1089 self.verifier.add(tx_hash, tx_height)
1090 # set the height in case it changed
1091 tx = self.transactions.get(tx_hash)
1093 if tx.get('height') != tx_height:
1094 print_error( "changing height for tx", tx_hash )
1095 tx['height'] = tx_height
1098 def is_addr_in_tx(self, addr, tx):
1100 for txin in tx.get('inputs'):
1101 if addr == txin.get('address'):
1104 for txout in tx.get('outputs'):
1105 if addr == txout.get('address'):
1111 def check_new_history(self, addr, hist):
1113 # check that all tx in hist are relevant
1115 for tx_hash, height in hist:
1116 tx = self.transactions.get(tx_hash)
1118 if not self.is_addr_in_tx(addr,tx):
1121 # check that we are not "orphaning" a transaction
1122 old_hist = self.history.get(addr,[])
1123 if old_hist == ['*']: return True
1125 for tx_hash, height in old_hist:
1126 if tx_hash in map(lambda x:x[0], hist): continue
1128 for _addr, _hist in self.history.items():
1129 if _addr == addr: continue
1130 if _hist == ['*']: continue
1131 _tx_hist = map(lambda x:x[0], _hist)
1132 if tx_hash in _tx_hist:
1137 tx = self.transactions.get(tx_hash)
1138 # tx might not be there
1142 if tx.get('height'):
1145 print_error("new history is orphaning transaction:", tx_hash)
1146 # check that all outputs are not mine, request histories
1148 for o in tx.get('outputs'):
1149 _addr = o.get('address')
1150 # assert not self.is_mine(_addr)
1151 ext_requests.append( ('blockchain.address.get_history', [_addr]) )
1153 ext_h = self.interface.synchronous_get(ext_requests)
1156 if h == ['*']: continue
1158 if item.get('tx_hash') == tx_hash:
1159 height = item.get('height')
1160 tx['height'] = height
1162 print_error("found height for", tx_hash, height)
1163 self.verifier.add(tx_hash, height)
1165 print_error("removing orphaned tx from history", tx_hash)
1166 self.transactions.pop(tx_hash)
1172 def check_new_tx(self, tx_hash, tx):
1173 # 1 check that tx is referenced in addr_history.
1175 for addr, hist in self.history.items():
1176 if hist == ['*']:continue
1177 for txh, height in hist:
1179 addresses.append(addr)
1184 # 2 check that referencing addresses are in the tx
1185 for addr in addresses:
1186 if not self.is_addr_in_tx(addr, tx):
1194 class WalletSynchronizer(threading.Thread):
1197 def __init__(self, wallet, config):
1198 threading.Thread.__init__(self)
1200 self.wallet = wallet
1201 self.interface = self.wallet.interface
1202 self.interface.register_channel('synchronizer')
1203 self.wallet.interface.register_callback('connected', lambda: self.wallet.set_up_to_date(False))
1204 self.wallet.interface.register_callback('connected', lambda: self.interface.send([('server.banner',[])],'synchronizer') )
1205 self.was_updated = True
1206 self.running = False
1207 self.lock = threading.Lock()
1210 with self.lock: self.running = False
1211 self.interface.poke('synchronizer')
1213 def is_running(self):
1214 with self.lock: return self.running
1216 def synchronize_wallet(self):
1217 new_addresses = self.wallet.synchronize()
1219 self.subscribe_to_addresses(new_addresses)
1220 self.wallet.up_to_date = False
1223 if not self.interface.is_up_to_date('synchronizer'):
1224 if self.wallet.is_up_to_date():
1225 self.wallet.set_up_to_date(False)
1226 self.was_updated = True
1229 self.wallet.set_up_to_date(True)
1230 self.was_updated = True
1233 def subscribe_to_addresses(self, addresses):
1235 for addr in addresses:
1236 messages.append(('blockchain.address.subscribe', [addr]))
1237 self.interface.send( messages, 'synchronizer')
1241 with self.lock: self.running = True
1245 requested_histories = {}
1247 # request any missing transactions
1248 for history in self.wallet.history.values():
1249 if history == ['*']: continue
1250 for tx_hash, tx_height in history:
1251 if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
1252 missing_tx.append( (tx_hash, tx_height) )
1253 print_error("missing tx", missing_tx)
1255 # wait until we are connected, in case the user is not connected
1256 while not self.interface.is_connected:
1259 # request banner, because 'connected' event happens before this thread is started
1260 self.interface.send([('server.banner',[])],'synchronizer')
1263 self.subscribe_to_addresses(self.wallet.all_addresses())
1265 while self.is_running():
1266 # 1. send new requests
1267 self.synchronize_wallet()
1269 for tx_hash, tx_height in missing_tx:
1270 if (tx_hash, tx_height) not in requested_tx:
1271 self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
1272 requested_tx.append( (tx_hash, tx_height) )
1275 if self.was_updated:
1276 self.interface.trigger_callback('updated')
1277 self.was_updated = False
1280 r = self.interface.get_response('synchronizer')
1282 # poke sends None. (needed during stop)
1285 # 3. handle response
1286 method = r['method']
1287 params = r['params']
1288 result = r.get('result')
1289 error = r.get('error')
1294 if method == 'blockchain.address.subscribe':
1296 if self.wallet.get_status(self.wallet.get_history(addr)) != result:
1297 if requested_histories.get(addr) is None:
1298 self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
1299 requested_histories[addr] = result
1301 elif method == 'blockchain.address.get_history':
1303 print_error("receiving history", addr, result)
1305 assert requested_histories.pop(addr) == '*'
1306 self.wallet.receive_history_callback(addr, result)
1309 # check that txids are unique
1312 tx_hash = item['tx_hash']
1313 if tx_hash not in txids:
1314 txids.append(tx_hash)
1315 hist.append( (tx_hash, item['height']) )
1317 if len(hist) != len(result):
1318 raise BaseException("error: server sent history with non-unique txid", result)
1320 # check that the status corresponds to what was announced
1321 rs = requested_histories.pop(addr)
1322 if self.wallet.get_status(hist) != rs:
1323 raise BaseException("error: status mismatch: %s"%addr)
1325 # store received history
1326 self.wallet.receive_history_callback(addr, hist)
1328 # request transactions that we don't have
1329 for tx_hash, tx_height in hist:
1330 if self.wallet.transactions.get(tx_hash) is None:
1331 if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
1332 missing_tx.append( (tx_hash, tx_height) )
1334 elif method == 'blockchain.transaction.get':
1336 tx_height = params[1]
1337 assert tx_hash == hash_encode(Hash(result.decode('hex')))
1338 tx = Transaction(result)
1339 d = tx.deserialize()
1340 d['height'] = tx_height
1341 d['tx_hash'] = tx_hash
1342 self.wallet.receive_tx_callback(tx_hash, d)
1343 self.was_updated = True
1344 requested_tx.remove( (tx_hash, tx_height) )
1345 print_error("received tx:", d)
1347 elif method == 'blockchain.transaction.broadcast':
1348 self.wallet.tx_result = result
1349 self.wallet.tx_event.set()
1351 elif method == 'server.banner':
1352 self.wallet.banner = result
1353 self.interface.trigger_callback('banner')
1355 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
1357 if self.was_updated and not requested_tx:
1358 self.interface.trigger_callback('updated')
1359 self.was_updated = False