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/>.
20 import sys, base64, os, re, hashlib, socket, getpass, copy, operator, urllib2, ast
25 print "python-ecdsa does not seem to be installed. Try 'sudo easy_install ecdsa'"
32 has_encryption = False
35 ############ functions from pywallet #####################
39 def hash_160(public_key):
40 md = hashlib.new('ripemd160')
41 md.update(hashlib.sha256(public_key).digest())
44 def public_key_to_bc_address(public_key):
45 h160 = hash_160(public_key)
46 return hash_160_to_bc_address(h160)
48 def hash_160_to_bc_address(h160):
49 vh160 = chr(addrtype) + h160
52 return b58encode(addr)
54 def bc_address_to_hash_160(addr):
55 bytes = b58decode(addr, 25)
58 __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
59 __b58base = len(__b58chars)
62 """ encode v, which is a string of bytes, to base58.
66 for (i, c) in enumerate(v[::-1]):
67 long_value += (256**i) * ord(c)
70 while long_value >= __b58base:
71 div, mod = divmod(long_value, __b58base)
72 result = __b58chars[mod] + result
74 result = __b58chars[long_value] + result
76 # Bitcoin does a little leading-zero-compression:
77 # leading 0-bytes in the input become leading-1s
80 if c == '\0': nPad += 1
83 return (__b58chars[0]*nPad) + result
85 def b58decode(v, length):
86 """ decode v into a string of len bytes
89 for (i, c) in enumerate(v[::-1]):
90 long_value += __b58chars.find(c) * (__b58base**i)
93 while long_value >= 256:
94 div, mod = divmod(long_value, 256)
95 result = chr(mod) + result
97 result = chr(long_value) + result
101 if c == __b58chars[0]: nPad += 1
104 result = chr(0)*nPad + result
105 if length is not None and len(result) != length:
112 return hashlib.sha256(hashlib.sha256(data).digest()).digest()
114 def EncodeBase58Check(vchIn):
116 return b58encode(vchIn + hash[0:4])
118 def DecodeBase58Check(psz):
119 vchRet = b58decode(psz, None)
129 def PrivKeyToSecret(privkey):
130 return privkey[9:9+32]
132 def SecretToASecret(secret):
133 vchIn = chr(addrtype+128) + secret
134 return EncodeBase58Check(vchIn)
136 def ASecretToSecret(key):
137 vch = DecodeBase58Check(key)
138 if vch and vch[0] == chr(addrtype+128):
143 ########### end pywallet functions #######################
146 def int_to_hex(i, length=1):
148 s = "0"*(2*length - len(s)) + s
149 return s.decode('hex')[::-1].encode('hex')
152 # password encryption
153 from Crypto.Cipher import AES
156 pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING
157 EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
158 DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING)
161 # secp256k1, http://www.oid-info.com/get/1.3.132.0.10
162 _p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL
163 _r = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L
164 _b = 0x0000000000000000000000000000000000000000000000000000000000000007L
165 _a = 0x0000000000000000000000000000000000000000000000000000000000000000L
166 _Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L
167 _Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L
168 curve_secp256k1 = ecdsa.ellipticcurve.CurveFp( _p, _a, _b )
169 generator_secp256k1 = ecdsa.ellipticcurve.Point( curve_secp256k1, _Gx, _Gy, _r )
170 oid_secp256k1 = (1,3,132,0,10)
171 SECP256k1 = ecdsa.curves.Curve("SECP256k1", curve_secp256k1, generator_secp256k1, oid_secp256k1 )
174 no_wallet_message = "Wallet file not found.\nPlease provide a seed and a password. The seed will be to generate Bitcoin addresses. It should be long and random, and nobody should be able to guess it. Memorize it, or write it down and keep it in a vault. The password will be used to encrypt your local wallet file. You will need to enter your password everytime you use your wallet. If you lose your password, you can still recover your wallet with the seed."
177 out = re.sub('( [^\n]*|)\n','',s)
178 out = out.replace(' ','')
179 out = out.replace('\n','')
182 def raw_tx( inputs, outputs, for_sig = None ):
183 s = int_to_hex(1,4) + ' version\n'
184 s += int_to_hex( len(inputs) ) + ' number of inputs\n'
185 for i in range(len(inputs)):
186 _, _, p_hash, p_index, p_script, pubkey, sig = inputs[i]
187 s += p_hash.decode('hex')[::-1].encode('hex') + ' prev hash\n'
188 s += int_to_hex(p_index,4) + ' prev index\n'
190 sig = sig + chr(1) # hashtype
191 script = int_to_hex( len(sig)) + ' push %d bytes\n'%len(sig)
192 script += sig.encode('hex') + ' sig\n'
193 pubkey = chr(4) + pubkey
194 script += int_to_hex( len(pubkey)) + ' push %d bytes\n'%len(pubkey)
195 script += pubkey.encode('hex') + ' pubkey\n'
197 script = p_script + ' scriptsig \n'
200 s += int_to_hex( len(filter(script))/2 ) + ' script length \n'
202 s += "ffffffff" + ' sequence\n'
203 s += int_to_hex( len(outputs) ) + ' number of outputs\n'
204 for output in outputs:
205 addr, amount = output
206 s += int_to_hex( amount, 8) + ' amount: %d\n'%amount
207 script = '76a9' # op_dup, op_hash_160
208 script += '14' # push 0x14 bytes
209 script += bc_address_to_hash_160(addr).encode('hex')
210 script += '88ac' # op_equalverify, op_checksig
211 s += int_to_hex( len(filter(script))/2 ) + ' script length \n'
212 s += script + ' script \n'
213 s += int_to_hex(0,4) # lock time
214 if for_sig is not None: s += int_to_hex(1, 4) # hash type
217 class InvalidPassword(Exception):
220 wallet_dir = os.environ["HOME"] + '/.bitcoin/'
221 if not os.path.exists( wallet_dir ):
222 os.mkdir( wallet_dir )
223 wallet_path = wallet_dir + '/electrum.dat'
227 self.gap_limit = 5 # configuration
228 self.host = 'ecdsa.org'
233 self.use_encryption = False
235 self.seed = '' # encrypted
236 self.private_keys = repr([]) # encrypted
237 self.change_addresses = [] # index of addresses used as change
238 self.status = {} # current status of addresses
240 self.labels = {} # labels for addresses and transactions
241 self.addressbook = [] # outgoing addresses, for payments
248 def is_mine(self, address):
249 return address in self.addresses
251 def is_change(self, address):
252 if not self.is_mine(address):
254 k = self.addresses.index(address)
255 return k in self.change_addresses
257 def is_valid(self,addr):
258 ADDRESS_RE = re.compile('[1-9A-HJ-NP-Za-km-z]{26,}\\Z')
259 return ADDRESS_RE.match(addr)
261 def create_new_address(self, for_change, password):
262 seed = self.pw_decode( self.seed, password)
263 i = len( self.addresses ) - len(self.change_addresses) if not for_change else len(self.change_addresses)
264 seed = Hash( "%d:%d:"%(i,for_change) + seed )
265 order = generator_secp256k1.order()
266 secexp = ecdsa.util.randrange_from_seed__trytryagain( seed, order )
267 secret = SecretToASecret( ('%064x' % secexp).decode('hex') )
268 private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
269 public_key = private_key.get_verifying_key()
270 address = public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() )
272 private_keys = ast.literal_eval( self.pw_decode( self.private_keys, password) )
273 private_keys.append(secret)
275 raise InvalidPassword("")
276 self.private_keys = self.pw_encode( repr(private_keys), password)
277 self.addresses.append(address)
278 if for_change: self.change_addresses.append( i )
279 h = self.retrieve_history(address)
280 self.history[address] = h
281 self.status[address] = h[-1]['blk_hash'] if h else None
284 def recover(self, password):
285 seed = self.pw_decode( self.seed, password)
286 # todo: recover receiving addresses from tx
289 addr = self.create_new_address(True, password)
290 print "recovering", addr
291 if self.status[addr] is None: break
295 addr = self.create_new_address(False, password)
296 print "recovering", addr
297 if self.status[addr] is None:
299 if num_gap == self.gap_limit: break
303 # remove limit-1 addresses. [ this is ok, because change addresses are at the beginning of the list]
305 self.addresses = self.addresses[:-n]
306 private_keys = ast.literal_eval( self.pw_decode( self.private_keys, password))
307 private_keys = private_keys[:-n]
308 self.private_keys = self.pw_encode( repr(private_keys), password)
310 # history and addressbook
311 self.update_tx_history()
312 for tx in self.tx_history.values():
314 for i in tx['outputs']:
315 if not self.is_mine(i) and i not in self.addressbook:
316 self.addressbook.append(i)
318 self.update_tx_labels()
321 s = repr( (self.use_encryption, self.fee, self.host, self.blocks,
322 self.seed, self.addresses, self.private_keys,
323 self.change_addresses, self.status, self.history,
324 self.labels, self.addressbook) )
325 f = open(wallet_path,"w")
331 f = open(wallet_path,"r")
337 (self.use_encryption, self.fee, self.host, self.blocks,
338 self.seed, self.addresses, self.private_keys,
339 self.change_addresses, self.status, self.history,
340 self.labels, self.addressbook) = ast.literal_eval( data )
343 self.update_tx_history()
346 def get_new_address(self, password):
348 for addr in self.addresses[-self.gap_limit:]:
349 if self.history[addr] == []:
351 if n < self.gap_limit:
353 new_address = self.create_new_address(False, password)
354 except InvalidPassword:
355 return False, "wrong password"
357 return True, new_address
359 return False, "The last %d addresses in your list have never been used. You should use them first, or increase the allowed gap size in your preferences. "%self.gap_limit
361 def get_addr_balance(self, addr):
362 h = self.history.get(addr)
372 def get_balance(self):
374 for addr in self.addresses:
375 c, u = self.get_addr_balance(addr)
380 def request(self, request ):
384 out = urllib2.urlopen('http://'+self.host+'/q/tw', request, timeout=5).read()
388 s = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
389 s.connect(( self.host, self.port))
398 if re.match('[^:]\s*\(', out): out = ''
401 def retrieve_message(self):
403 self.message = self.request( repr ( ('msg', '')))
405 def send_tx(self, data):
406 return self.request( repr ( ('tx', data )))
408 def retrieve_history(self, address):
409 return ast.literal_eval( self.request( repr ( ('h', address ))) )
412 return ast.literal_eval( self.request( repr ( ('poll', '' ))))
414 def new_session(self):
415 self.message = self.request( repr ( ('watch', repr(self.addresses) )))
418 blocks, changed_addresses = self.poll()
420 for addr, blk_hash in changed_addresses.items():
421 if self.status[addr] != blk_hash:
422 print "updating history for", addr
423 self.history[addr] = self.retrieve_history(addr)
424 self.status[addr] = blk_hash
425 self.update_tx_history()
426 if changed_addresses:
431 def choose_inputs_outputs( self, to_addr, amount, fee, password):
432 """ todo: minimize tx size """
434 amount = int( 1e8*amount )
438 for addr in self.addresses:
439 h = self.history.get(addr)
441 if item.get('raw_scriptPubKey'):
442 v = item.get('value')
444 inputs.append((addr, v, item['tx_hash'], item['pos'], item['raw_scriptPubKey'], None, None) )
445 if total >= amount + fee: break
446 if total >= amount + fee: break
448 print "not enough funds: %d %d"%(total, fee)
449 return False, "not enough funds: %d %d"%(total, fee)
450 outputs = [ (to_addr, amount) ]
451 change_amount = total - ( amount + fee )
452 if change_amount != 0:
453 # first look for unused change addresses
454 for addr in self.addresses:
455 i = self.addresses.index(addr)
456 if i not in self.change_addresses: continue
457 if self.history.get(addr): continue
458 change_address = addr
461 change_address = self.create_new_address(True, password)
462 print "new change address", change_address
463 outputs.append( (change_address, change_amount) )
464 return inputs, outputs
466 def sign_inputs( self, inputs, outputs, password ):
468 for i in range(len(inputs)):
469 addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i]
470 private_key = self.get_private_key(addr, password)
471 public_key = private_key.get_verifying_key()
472 pubkey = public_key.to_string()
473 tx = filter( raw_tx( inputs, outputs, for_sig = i ) )
474 sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
475 assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
476 s_inputs.append( (addr, v, p_hash, p_pos, p_scriptPubKey, pubkey, sig) )
479 def pw_encode(self, s, password):
481 secret = Hash(password)
482 cipher = AES.new(secret)
483 return EncodeAES(cipher, s)
487 def pw_decode(self, s, password):
489 secret = Hash(password)
490 cipher = AES.new(secret)
491 return DecodeAES(cipher, s)
495 def get_private_key( self, addr, password ):
497 private_keys = ast.literal_eval( self.pw_decode( self.private_keys, password ) )
499 raise InvalidPassword("")
500 k = self.addresses.index(addr)
501 secret = private_keys[k]
502 b = ASecretToSecret(secret)
503 secexp = int( b.encode('hex'), 16)
504 private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve=SECP256k1 )
505 public_key = private_key.get_verifying_key()
506 assert addr == public_key_to_bc_address( chr(4) + public_key.to_string() )
509 def get_tx_history(self):
510 lines = self.tx_history.values()
511 lines = sorted(lines, key=operator.itemgetter("nTime"))
514 def update_tx_history(self):
516 for addr in self.addresses:
517 for tx in self.history[addr]:
518 tx_hash = tx['tx_hash']
519 line = self.tx_history.get(tx_hash)
521 self.tx_history[tx_hash] = copy.copy(tx)
522 line = self.tx_history.get(tx_hash)
524 line['value'] += tx['value']
525 if line['blk_hash'] == 'mempool':
527 self.update_tx_labels()
529 def update_tx_labels(self):
530 for tx in self.tx_history.values():
533 for o_addr in tx['outputs']:
534 if not self.is_change(o_addr):
535 dest_label = self.labels.get(o_addr)
537 default_label = 'to: ' + dest_label
539 default_label = 'to: ' + o_addr
541 for o_addr in tx['outputs']:
542 if self.is_mine(o_addr) and not self.is_change(o_addr):
543 dest_label = self.labels.get(o_addr)
545 default_label = 'at: ' + dest_label
547 default_label = 'at: ' + o_addr
548 tx['default_label'] = default_label
552 def send(self, to_address, amount, label, password):
554 inputs, outputs = wallet.choose_inputs_outputs( to_address, amount, self.fee, password )
555 except InvalidPassword: return False, "Wrong password"
556 if not inputs: return False, "Not enough funds"
558 s_inputs = wallet.sign_inputs( inputs, outputs, password )
559 except InvalidPassword:
560 return False, "Wrong password"
561 tx = raw_tx( s_inputs, outputs )
563 tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
564 out = self.send_tx(tx)
566 return False, "error: hash mismatch"
567 if to_address not in self.addressbook:
568 self.addressbook.append(to_address)
570 wallet.labels[tx_hash] = label
575 if __name__ == '__main__':
581 known_commands = ['balance', 'sendtoaddress', 'password', 'getnewaddress', 'addresses', 'history', 'label', 'gui', 'all_addresses']
582 if cmd not in known_commands:
583 print "Known commands:", ', '.join(known_commands)
589 gui.init_wallet(wallet)
590 gui = gui.BitcoinGUI(wallet)
593 if not wallet.read():
594 print no_wallet_message
595 seed = raw_input("Enter seed: ")
597 print "Seed too short. Please at least 20 characters"
600 password = getpass.getpass("Password (hit return if you do not wish to encrypt your wallet):")
602 password2 = getpass.getpass("Confirm password:")
603 if password != password2:
608 print "in order to use wallet encryption, please install pycrypto (sudo easy_install pycrypto)"
610 wallet.seed = wallet.pw_encode( seed, password)
612 print "server name and port number (default: ecdsa.org:50000)"
613 host = raw_input("server:")
614 if not host: host = 'ecdsa.org'
616 port = raw_input("port:")
617 if not port: port = 50000
618 else: port = int(port)
620 print "default fee for transactions (default 0.005)"
621 fee = raw_input("default fee:")
622 if not fee: fee = 0.005
628 wallet.recover(password)
635 if cmd in ['sendtoaddress', 'password', 'getnewaddress']:
636 password = getpass.getpass('Password:') if wallet.use_encryption else None
639 c, u = wallet.get_balance()
645 elif cmd in [ 'addresses', 'all_addresses']:
646 for addr in wallet.addresses:
647 if cmd == 'all_addresses' or not wallet.is_change(addr):
648 label = wallet.labels.get(addr) if not wallet.is_change(addr) else "[change]"
649 if label is None: label = ''
650 h = wallet.history.get(addr)
653 if item['is_in']: ni += 1
655 print addr, no, ni, wallet.get_addr_balance(addr)[0]*1e-8, label
658 lines = wallet.get_tx_history()
662 v = 1.*line['value']/1e8
664 v_str = "%f"%v if v<0 else "+%f"%v
666 time_str = datetime.datetime.fromtimestamp( line['nTime'])
670 label = line.get('label')
671 if not label: label = line['tx_hash']
672 else: label = label + ' '*(64 - len(label) )
674 print time_str, " ", label, " ", v_str, " ", "%f"%b
675 print "# balance: ", b
680 label = ' '.join(sys.argv[3:])
682 print "syntax: label <tx_hash> <text>"
684 wallet.labels[tx] = label
687 elif cmd == 'sendtoaddress':
689 to_address = sys.argv[2]
690 amount = float(sys.argv[3])
691 label = ' '.join(sys.argv[4:])
693 print "syntax: send <recipient> <amount> [label]"
695 r, h = wallet.send( to_address, amount, label, password )
698 elif cmd == 'getnewaddress':
699 a = wallet.get_new_address()
703 print "Maximum gap reached. Increase gap in order to create more addresses."
705 elif cmd == 'password':
707 seed = wallet.pw_decode( wallet.seed, password)
708 private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
712 new_password = getpass.getpass('New password:')
713 if new_password == getpass.getpass('Confirm new password:'):
714 wallet.use_encryption = (new_password != '')
715 wallet.seed = wallet.pw_encode( seed, new_password)
716 wallet.private_keys = wallet.pw_encode( repr( private_keys ), new_password)
719 print "error: mismatch"