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 passphrase and a password. The passphrase will be used as a seed 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 passphrase."
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_path = os.environ["HOME"] + '/.bitcoin/electrum.dat'
224 self.gap_limit = 5 # configuration
225 self.host = 'ecdsa.org'
230 self.use_encryption = False
232 self.passphrase = '' # encrypted
233 self.private_keys = repr([]) # encrypted
234 self.change_addresses = [] # index of addresses used as change
235 self.status = {} # current status of addresses
237 self.labels = {} # labels for addresses and transactions
238 self.addressbook = [] # outgoing addresses, for payments
245 def is_mine(self, address):
246 return address in self.addresses
248 def is_change(self, address):
249 if not self.is_mine(address):
251 k = self.addresses.index(address)
252 return k in self.change_addresses
254 def is_valid(self,addr):
255 ADDRESS_RE = re.compile('[1-9A-HJ-NP-Za-km-z]{26,}\\Z')
256 return ADDRESS_RE.match(addr)
258 def create_new_address(self, for_change, password):
259 passphrase = self.pw_decode( self.passphrase, password)
260 i = len( self.addresses ) - len(self.change_addresses) if not for_change else len(self.change_addresses)
261 seed = Hash( "%d:%d:"%(i,for_change) + passphrase )
262 order = generator_secp256k1.order()
263 secexp = ecdsa.util.randrange_from_seed__trytryagain( seed, order )
264 secret = SecretToASecret( ('%064x' % secexp).decode('hex') )
265 private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
266 public_key = private_key.get_verifying_key()
267 address = public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() )
269 private_keys = ast.literal_eval( self.pw_decode( self.private_keys, password) )
270 private_keys.append(secret)
272 raise InvalidPassword("")
273 self.private_keys = self.pw_encode( repr(private_keys), password)
274 self.addresses.append(address)
275 if for_change: self.change_addresses.append( i )
276 h = self.retrieve_history(address)
277 self.history[address] = h
278 self.status[address] = h[-1]['blk_hash'] if h else None
281 def recover(self, password):
282 passphrase = self.pw_decode( self.passphrase, password)
283 # todo: recover receiving addresses from tx
286 addr = self.create_new_address(True, password)
287 print "recovering", addr
288 if self.status[addr] is None: break
292 addr = self.create_new_address(False, password)
293 print "recovering", addr
294 if self.status[addr] is None:
296 if num_gap == self.gap_limit: break
300 # remove limit-1 addresses. [ this is ok, because change addresses are at the beginning of the list]
302 self.addresses = self.addresses[:-n]
303 self.keys = self.private_keys[:-n]
305 # history and addressbook
306 self.update_tx_history()
307 for tx in self.tx_history.values():
309 for i in tx['outputs']:
310 if not self.is_mine(i) and i not in self.addressbook:
311 self.addressbook.append(i)
313 self.update_tx_labels()
316 s = repr( (self.use_encryption, self.fee, self.host, self.blocks,
317 self.passphrase, self.addresses, self.private_keys,
318 self.change_addresses, self.status, self.history,
319 self.labels, self.addressbook) )
320 f = open(wallet_path,"w")
326 f = open(wallet_path,"r")
332 (self.use_encryption, self.fee, self.host, self.blocks,
333 self.passphrase, self.addresses, self.private_keys,
334 self.change_addresses, self.status, self.history,
335 self.labels, self.addressbook) = ast.literal_eval( data )
338 self.update_tx_history()
341 def get_new_address(self, password):
343 for addr in self.addresses[-self.gap_limit:]:
344 if self.history[addr] == []:
346 if n < self.gap_limit:
348 new_address = self.create_new_address(False, password)
349 except InvalidPassword:
350 return False, "wrong password"
352 return True, new_address
354 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
356 def get_addr_balance(self, addr):
357 h = self.history.get(addr)
367 def get_balance(self):
369 for addr in self.addresses:
370 c, u = self.get_addr_balance(addr)
375 def request(self, request ):
379 out = urllib2.urlopen('http://'+self.host+'/q/tw', request, timeout=5).read()
383 s = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
384 s.connect(( self.host, self.port))
393 if re.match('[^:]\s*\(', out): out = ''
396 def retrieve_message(self):
398 self.message = self.request( repr ( ('msg', '')))
400 def send_tx(self, data):
401 return self.request( repr ( ('tx', data )))
403 def retrieve_history(self, address):
404 return ast.literal_eval( self.request( repr ( ('h', address ))) )
407 return ast.literal_eval( self.request( repr ( ('poll', '' ))))
409 def new_session(self):
410 self.message = self.request( repr ( ('watch', repr(self.addresses) )))
413 blocks, changed_addresses = self.poll()
415 for addr, blk_hash in changed_addresses.items():
416 if self.status[addr] != blk_hash:
417 print "updating history for", addr
418 self.history[addr] = self.retrieve_history(addr)
419 self.status[addr] = blk_hash
420 self.update_tx_history()
421 if changed_addresses:
426 def choose_inputs_outputs( self, to_addr, amount, fee, password):
427 """ todo: minimize tx size """
429 amount = int( 1e8*amount )
433 for addr in self.addresses:
434 h = self.history.get(addr)
436 if item.get('raw_scriptPubKey'):
437 v = item.get('value')
439 inputs.append((addr, v, item['tx_hash'], item['pos'], item['raw_scriptPubKey'], None, None) )
440 if total >= amount + fee: break
441 if total >= amount + fee: break
443 print "not enough funds: %d %d"%(total, fee)
444 return False, "not enough funds: %d %d"%(total, fee)
445 outputs = [ (to_addr, amount) ]
446 change_amount = total - ( amount + fee )
447 if change_amount != 0:
448 # first look for unused change addresses
449 for addr in self.addresses:
450 i = self.addresses.index(addr)
451 if i not in self.change_addresses: continue
452 if self.history.get(addr): continue
453 change_address = addr
456 change_address = self.create_new_address(True, password)
457 print "new change address", change_address
458 outputs.append( (change_address, change_amount) )
459 return inputs, outputs
461 def sign_inputs( self, inputs, outputs, password ):
463 for i in range(len(inputs)):
464 addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i]
465 private_key = self.get_private_key(addr, password)
466 public_key = private_key.get_verifying_key()
467 pubkey = public_key.to_string()
468 tx = filter( raw_tx( inputs, outputs, for_sig = i ) )
469 sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
470 assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
471 s_inputs.append( (addr, v, p_hash, p_pos, p_scriptPubKey, pubkey, sig) )
474 def pw_encode(self, s, password):
476 secret = Hash(password)
477 cipher = AES.new(secret)
478 return EncodeAES(cipher, s)
482 def pw_decode(self, s, password):
484 secret = Hash(password)
485 cipher = AES.new(secret)
486 return DecodeAES(cipher, s)
490 def get_private_key( self, addr, password ):
492 private_keys = ast.literal_eval( self.pw_decode( self.private_keys, password ) )
494 raise InvalidPassword("")
495 k = self.addresses.index(addr)
496 secret = private_keys[k]
497 b = ASecretToSecret(secret)
498 secexp = int( b.encode('hex'), 16)
499 private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve=SECP256k1 )
500 public_key = private_key.get_verifying_key()
501 assert addr == public_key_to_bc_address( chr(4) + public_key.to_string() )
504 def get_tx_history(self):
505 lines = self.tx_history.values()
506 lines = sorted(lines, key=operator.itemgetter("nTime"))
509 def update_tx_history(self):
511 for addr in self.addresses:
512 for tx in self.history[addr]:
513 tx_hash = tx['tx_hash']
514 line = self.tx_history.get(tx_hash)
516 self.tx_history[tx_hash] = copy.copy(tx)
517 line = self.tx_history.get(tx_hash)
519 line['value'] += tx['value']
520 if line['blk_hash'] == 'mempool':
522 self.update_tx_labels()
524 def update_tx_labels(self):
525 for tx in self.tx_history.values():
528 for o_addr in tx['outputs']:
529 if not self.is_change(o_addr):
530 dest_label = self.labels.get(o_addr)
532 default_label = 'to: ' + dest_label
534 default_label = 'to: ' + o_addr
536 for o_addr in tx['outputs']:
537 if self.is_mine(o_addr) and not self.is_change(o_addr):
538 dest_label = self.labels.get(o_addr)
540 default_label = 'at: ' + dest_label
542 default_label = 'at: ' + o_addr
543 tx['default_label'] = default_label
547 def send(self, to_address, amount, label, password):
549 inputs, outputs = wallet.choose_inputs_outputs( to_address, amount, self.fee, password )
550 except InvalidPassword: return False, "Wrong password"
551 if not inputs: return False, "Not enough funds"
553 s_inputs = wallet.sign_inputs( inputs, outputs, password )
554 except InvalidPassword:
555 return False, "Wrong password"
556 tx = raw_tx( s_inputs, outputs )
558 tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
559 out = self.send_tx(tx)
561 return False, "error: hash mismatch"
562 wallet.labels[tx_hash] = label
567 if __name__ == '__main__':
573 known_commands = ['balance', 'sendtoaddress', 'password', 'getnewaddress', 'addresses', 'history', 'label', 'gui', 'all_addresses']
574 if cmd not in known_commands:
575 print "Known commands:", ', '.join(known_commands)
581 gui.init_wallet(wallet)
582 gui = gui.BitcoinGUI(wallet)
586 if not wallet.read():
587 print no_wallet_message
588 passphrase = raw_input("Enter passphrase: ")
589 if len(passphrase)<20:
590 print "Passphrase too short. Please at least 20 characters"
593 password = getpass.getpass("Password (hit return if you do not wish to encrypt your wallet):")
595 password2 = getpass.getpass("Confirm password:")
596 if password != password2:
601 print "in order to use wallet encryption, please install pycrypto (sudo easy_install pycrypto)"
603 wallet.passphrase = wallet.pw_encode( passphrase, password)
605 print "server name and port number (default: ecdsa.org:50000)"
606 host = raw_input("server:")
607 if not host: host = 'ecdsa.org'
609 port = raw_input("port:")
610 if not port: port = 50000
611 else: port = int(port)
613 print "default fee for transactions (default 0.005)"
614 fee = raw_input("default fee:")
615 if not fee: fee = 0.005
621 wallet.recover(password)
628 if cmd in ['sendtoaddress', 'password', 'getnewaddress']:
629 password = getpass.getpass('Password:') if wallet.use_encryption else None
632 c, u = wallet.get_balance()
638 elif cmd in [ 'addresses', 'all_addresses']:
639 for addr in wallet.addresses:
640 if cmd == 'all_addresses' or not wallet.is_change(addr):
641 label = wallet.labels.get(addr) if not wallet.is_change(addr) else "[change]"
642 if label is None: label = ''
643 h = wallet.history.get(addr)
646 if item['is_in']: ni += 1
648 print addr, no, ni, wallet.get_addr_balance(addr)[0]*1e-8, label
651 lines = wallet.get_tx_history()
655 v = 1.*line['value']/1e8
657 v_str = "%f"%v if v<0 else "+%f"%v
659 time_str = datetime.datetime.fromtimestamp( line['nTime'])
663 label = line.get('label')
664 if not label: label = line['tx_hash']
665 else: label = label + ' '*(64 - len(label) )
667 print time_str, " ", label, " ", v_str, " ", "%f"%b
668 print "# balance: ", b
673 label = ' '.join(sys.argv[3:])
675 print "syntax: label <tx_hash> <text>"
677 wallet.labels[tx] = label
680 elif cmd == 'sendtoaddress':
682 to_address = sys.argv[2]
683 amount = float(sys.argv[3])
684 label = ' '.join(sys.argv[4:])
686 print "syntax: send <recipient> <amount> [label]"
688 r, h = wallet.send( to_address, amount, label, password )
691 elif cmd == 'getnewaddress':
692 a = wallet.get_new_address()
696 print "Maximum gap reached. Increase gap in order to create more addresses."
698 elif cmd == 'password':
700 passphrase = wallet.pw_decode( wallet.passphrase, password)
701 private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
705 new_password = getpass.getpass('New password:')
706 if new_password == getpass.getpass('Confirm new password:'):
707 wallet.use_encryption = (new_password != '')
708 wallet.passphrase = wallet.pw_encode( passphrase, new_password)
709 wallet.private_keys = wallet.pw_encode( repr( private_keys ), new_password)
712 print "error: mismatch"