electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit eb060854a156d72b5af8f1c029f805a3e476fcb4
parent b0b4dfaa186dc1b8621ff947051360447320c092
Author: ThomasV <thomasv@gitorious>
Date:   Tue,  7 Feb 2012 19:48:15 +0300

Merge branch 'master' of gitorious.org:electrum/electrum

Diffstat:
Mclient/electrum | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mclient/gui.py | 324+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mclient/interface.py | 3+++
Aclient/msqr.py | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mclient/version.py | 2+-
Mclient/wallet.py | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
6 files changed, 545 insertions(+), 120 deletions(-)

diff --git a/client/electrum b/client/electrum @@ -16,15 +16,22 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import re,sys +import re, sys, getpass from optparse import OptionParser - from wallet import Wallet from interface import Interface +from decimal import Decimal + +# URL decode +_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) +urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) + + +from wallet import format_satoshis if __name__ == '__main__': - known_commands = ['help', 'validateaddress', 'balance', 'contacts', 'create', 'payto', 'sendtx', 'password', 'newaddress', 'addresses', 'history', 'label', 'gui', 'mktx','seed','import'] + known_commands = ['help', 'validateaddress', 'balance', 'contacts', 'create', 'payto', 'sendtx', 'password', 'newaddress', 'addresses', 'history', 'label', 'gui', 'mktx','seed','import','signmessage','verifymessage','eval'] usage = "usage: %prog [options] command args\nCommands: "+ (', '.join(known_commands)) @@ -52,24 +59,30 @@ if __name__ == '__main__': import gui gui.init_wallet(wallet) gui = gui.BitcoinGUI(wallet) + if re.match('^bitcoin:', cmd): + o = cmd[8:].split('?') address = o[0] if len(o)>1: params = o[1].split('&') else: params = [] - cmd = 'gui' - amount = '' - label = '' + + amount = label = message = signature = identity = '' for p in params: k,v = p.split('=') - v = urldecode(v) - if k=='amount': amount = v - elif k=='label': label = v - else: print k,v - - gui.set_send_tab(address, amount, label) + uv = urldecode(v) + if k == 'amount': amount = uv + elif k == 'message': message = uv + elif k == 'label': label = uv + elif k == 'signature': + identity, signature = uv.split(':') + cmd = cmd.replace('&%s=%s'%(k,v),'') + else: + print k,v + + gui.set_send_tab(address, amount, message, label, identity, signature, cmd) gui.main() wallet.save() @@ -98,7 +111,7 @@ if __name__ == '__main__': host = raw_input("server (default:%s):"%wallet.interface.host) port = raw_input("port (default:%d):"%wallet.interface.port) - fee = raw_input("fee (default:%f):"%(wallet.fee*1e-8)) + fee = raw_input("fee (default:%s):"%( str(Decimal(wallet.fee)/100000000)) ) if fee: wallet.fee = float(fee) if host: wallet.interface.host = host if port: wallet.interface.port = int(port) @@ -136,13 +149,13 @@ if __name__ == '__main__': cmd = 'help' # open session - if cmd not in ['password', 'mktx', 'history', 'label', 'contacts', 'help', 'validateaddress']: + if cmd not in ['password', 'mktx', 'history', 'label', 'contacts', 'help', 'validateaddress', 'signmessage', 'verifymessage', 'eval']: wallet.interface.new_session(wallet.all_addresses(), wallet.electrum_version) wallet.update() wallet.save() # commands needing password - if cmd in ['payto', 'password', 'mktx', 'seed', 'import' ] or ( cmd=='addresses' and options.show_keys): + if cmd in ['payto', 'password', 'mktx', 'seed', 'import','signmessage' ] or ( cmd=='addresses' and options.show_keys): password = getpass.getpass('Password:') if wallet.use_encryption else None # check password try: @@ -193,6 +206,8 @@ if __name__ == '__main__': print "syntax: mktx <recipient> <amount> [label]" elif cmd2 == 'seed': print "show generation seed of your wallet. password protected." + elif cmd2 == 'eval': + print "run python eval on an object" elif cmd == 'seed': import mnemonic @@ -211,21 +226,25 @@ if __name__ == '__main__': if addrs == []: c, u = wallet.get_balance() if u: - print c*1e-8, u*1e-8 + print Decimal( c ) / 100000000 , Decimal( u ) / 100000000 else: - print c*1e-8 + print Decimal( c ) / 100000000 else: for addr in addrs: c, u = wallet.get_addr_balance(addr) if u: - print "%s %s, %s" % (addr, c*1e-8, u*1e-8) + print "%s %s, %s" % (addr, str(Decimal(c)/100000000), str(Decimal(u)/100000000)) else: - print "%s %s" % (addr, c*1e-8) + print "%s %s" % (addr, str(Decimal(c)/100000000)) elif cmd in [ 'contacts']: for addr in wallet.addressbook: print addr, " ", wallet.labels.get(addr) + elif cmd == 'eval': + print eval(args[1]) + wallet.save() + elif cmd in [ 'addresses']: for addr in wallet.all_addresses(): if options.show_all or not wallet.is_change(addr): @@ -240,7 +259,7 @@ if __name__ == '__main__': for item in h: if item['is_in']: ni += 1 else: no += 1 - b = "%d %d %f"%(no, ni, wallet.get_addr_balance(addr)[0]*1e-8) + b = "%d %d %s"%(no, ni, str(Decimal(wallet.get_addr_balance(addr)[0])/100000000)) else: b='' if options.show_keys: pk = wallet.get_private_key2(addr, password) @@ -252,9 +271,8 @@ if __name__ == '__main__': b = 0 for line in lines: import datetime - v = 1.*line['value']/1e8 + v = line['value'] b += v - v_str = "%f"%v if v<0 else "+%f"%v try: time_str = datetime.datetime.fromtimestamp( line['nTime']) except: @@ -264,8 +282,8 @@ if __name__ == '__main__': if not label: label = line['tx_hash'] else: label = label + ' '*(64 - len(label) ) - print time_str, " ", label, " ", v_str, " ", "%f"%b - print "# balance: ", b + print time_str , " " + label + " " + format_satoshis(v)+ " "+ format_satoshis(b) + print "# balance: ", format_satoshis(b) elif cmd == 'label': try: @@ -323,3 +341,16 @@ if __name__ == '__main__': else: print "error: mismatch" + elif cmd == 'signmessage': + address, message = args[1:3] + print wallet.sign_message(address, message, password) + + elif cmd == 'verifymessage': + address, signature, message = args[1:4] + try: + wallet.verify_message(address, signature, message) + print True + except: + print False + + diff --git a/client/gui.py b/client/gui.py @@ -17,7 +17,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import datetime -import thread, time, ast, sys +import thread, time, ast, sys, re import socket, traceback import pygtk pygtk.require('2.0') @@ -28,12 +28,7 @@ from decimal import Decimal gtk.gdk.threads_init() APP_NAME = "Electrum" -def format_satoshis(x): - s = str( Decimal(x) /100000000 ) - if not '.' in s: s += '.' - p = s.find('.') - s += " "*( 9 - ( len(s) - p )) - return s +from wallet import format_satoshis def numbify(entry, is_int = False): text = entry.get_text().strip() @@ -124,6 +119,7 @@ def init_wallet(wallet): else: # ask for the server. + wallet.interface.get_servers() run_network_dialog( wallet, parent=None ) # ask for seed and gap. @@ -370,8 +366,9 @@ def run_network_dialog( wallet, parent ): if host!= wallet.interface.host or port!=wallet.interface.port: wallet.interface.host = host wallet.interface.set_port( port ) - wallet.save() wallet.interface.is_connected = False + if parent: + wallet.save() @@ -399,8 +396,8 @@ def password_line(label): password.show() return password, password_entry -def password_dialog(): - dialog = gtk.MessageDialog( None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, +def password_dialog(parent): + dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.") dialog.get_image().set_visible(False) current_pw, current_pw_entry = password_line('Password:') @@ -529,7 +526,7 @@ class BitcoinGUI: def seedb(w, wallet): if wallet.use_encryption: - password = password_dialog() + password = password_dialog(self.window) if not password: return else: password = None show_seed_dialog(wallet, password, self.window) @@ -568,7 +565,7 @@ class BitcoinGUI: self.window.add(vbox) self.window.show_all() - self.fee_box.hide() + #self.fee_box.hide() self.context_id = self.status_bar.get_context_id("statusbar") self.update_status_bar() @@ -578,6 +575,27 @@ class BitcoinGUI: gobject.idle_add( self.update_status_bar ) time.sleep(0.5) + + def check_recipient_thread(): + old_r = '' + while True: + time.sleep(0.5) + if self.payto_entry.is_focus(): + continue + r = self.payto_entry.get_text() + if r != old_r: + old_r = r + r = r.strip() + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): + try: + to_address = self.get_alias(r, interactive=False) + except: + continue + if to_address: + s = r + ' <' + to_address + '>' + gobject.idle_add( lambda: self.payto_entry.set_text(s) ) + + def update_wallet_thread(): while True: try: @@ -625,6 +643,7 @@ class BitcoinGUI: thread.start_new_thread(update_wallet_thread, ()) thread.start_new_thread(update_status_bar_thread, ()) + thread.start_new_thread(check_recipient_thread, ()) self.notebook.set_current_page(0) @@ -635,60 +654,68 @@ class BitcoinGUI: def create_send_tab(self): - + page = vbox = gtk.VBox() page.show() payto = gtk.HBox() payto_label = gtk.Label('Pay to:') - payto_label.set_size_request(100,10) - payto_label.show() + payto_label.set_size_request(100,-1) payto.pack_start(payto_label, False) payto_entry = gtk.Entry() - payto_entry.set_size_request(350, 26) - payto_entry.show() + payto_entry.set_size_request(450, 26) payto.pack_start(payto_entry, False) vbox.pack_start(payto, False, False, 5) - - label = gtk.HBox() - label_label = gtk.Label('Label:') - label_label.set_size_request(100,10) - label_label.show() - label.pack_start(label_label, False) - label_entry = gtk.Entry() - label_entry.set_size_request(350, 26) - label_entry.show() - label.pack_start(label_entry, False) - vbox.pack_start(label, False, False, 5) + + message = gtk.HBox() + message_label = gtk.Label('Description:') + message_label.set_size_request(100,-1) + message.pack_start(message_label, False) + message_entry = gtk.Entry() + message_entry.set_size_request(450, 26) + message.pack_start(message_entry, False) + vbox.pack_start(message, False, False, 5) amount_box = gtk.HBox() amount_label = gtk.Label('Amount:') amount_label.set_size_request(100,-1) - amount_label.show() amount_box.pack_start(amount_label, False) amount_entry = gtk.Entry() amount_entry.set_size_request(120, -1) - amount_entry.show() amount_box.pack_start(amount_entry, False) vbox.pack_start(amount_box, False, False, 5) - send_button = gtk.Button("Send") - send_button.show() - amount_box.pack_start(send_button, False, False, 5) - self.fee_box = fee_box = gtk.HBox() fee_label = gtk.Label('Fee:') - fee_label.set_size_request(100,10) + fee_label.set_size_request(100,-1) fee_box.pack_start(fee_label, False) fee_entry = gtk.Entry() - fee_entry.set_size_request(120, 26) - fee_entry.set_has_frame(False) - fee_entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee")) + fee_entry.set_size_request(60, 26) fee_box.pack_start(fee_entry, False) - - send_button.connect("clicked", self.do_send, (payto_entry, label_entry, amount_entry, fee_entry)) vbox.pack_start(fee_box, False, False, 5) + end_box = gtk.HBox() + empty_label = gtk.Label('') + empty_label.set_size_request(100,-1) + end_box.pack_start(empty_label, False) + send_button = gtk.Button("Send") + send_button.show() + end_box.pack_start(send_button, False, False, 0) + clear_button = gtk.Button("Clear") + clear_button.show() + end_box.pack_start(clear_button, False, False, 15) + send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry)) + clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry)) + + vbox.pack_start(end_box, False, False, 5) + + # display this line only if there is a signature + payto_sig = gtk.HBox() + payto_sig_id = gtk.Label('') + payto_sig.pack_start(payto_sig_id, False) + vbox.pack_start(payto_sig, True, True, 5) + + self.user_fee = False def entry_changed( entry, is_fee ): @@ -696,7 +723,8 @@ class BitcoinGUI: fee = numbify(fee_entry) if not is_fee: fee = None if amount is None: - self.fee_box.hide(); return + #self.fee_box.hide(); + return inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) if not is_fee: fee_entry.set_text( str( Decimal( fee ) / 100000000 ) ) @@ -713,25 +741,68 @@ class BitcoinGUI: self.error = 'Not enough funds' amount_entry.connect('changed', entry_changed, False) - fee_entry.connect('changed', entry_changed, True) + fee_entry.connect('changed', entry_changed, True) self.payto_entry = payto_entry - self.payto_amount_entry = amount_entry - self.payto_label_entry = label_entry + self.payto_fee_entry = fee_entry + self.payto_sig_id = payto_sig_id + self.payto_sig = payto_sig + self.amount_entry = amount_entry + self.message_entry = message_entry self.add_tab(page, 'Send') - def set_send_tab(self, address, amount, label): + def set_frozen(self,entry,frozen): + if frozen: + entry.set_editable(False) + entry.set_has_frame(False) + entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee")) + else: + entry.set_editable(True) + entry.set_has_frame(True) + entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff")) + + + def set_send_tab(self, payto, amount, message, label, identity, signature, cmd): self.notebook.set_current_page(1) - self.payto_entry.set_text(address) - self.payto_label_entry.set_text(label) - self.payto_amount_entry.set_text(amount) + + if signature: + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity): + signing_address = self.get_alias(identity, interactive = True) + elif self.wallet.is_valid(identity): + signing_address = identity + else: + signing_address = None + if not signing_address: + return + try: + self.wallet.verify_message(signing_address, signature, cmd ) + self.wallet.receipt = (signing_address, signature, cmd) + except: + self.show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.') + payto = amount = label = identity = message = '' + + # redundant with aliases + #if label and payto: + # self.labels[payto] = label + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', payto): + payto_address = self.get_alias(payto, interactive=True) + if payto_address: + payto = payto + ' <' + payto_address + '>' + + self.payto_entry.set_text(payto) + self.message_entry.set_text(message) + self.amount_entry.set_text(amount) + if identity: + self.set_frozen(self.payto_entry,True) + self.set_frozen(self.amount_entry,True) + self.set_frozen(self.message_entry,True) + self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity ) + else: + self.payto_sig.set_visible(False) def create_about_tab(self): page = gtk.VBox() page.show() - #self.info = gtk.Label('') - #self.info.set_selectable(True) - #page.pack_start(self.info) tv = gtk.TextView() tv.set_editable(False) tv.set_cursor_visible(False) @@ -739,14 +810,84 @@ class BitcoinGUI: self.info = tv.get_buffer() self.add_tab(page, 'Wall') + def do_clear(self, w, data): + self.payto_sig.set_visible(False) + self.payto_fee_entry.set_text('') + for entry in [self.payto_entry,self.amount_entry,self.message_entry]: + self.set_frozen(entry,False) + entry.set_text('') + + + def question(self,msg): + dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) + dialog.show() + result = dialog.run() + dialog.destroy() + return result == gtk.RESPONSE_OK + + def get_alias(self, alias, interactive = False): + try: + target, signing_address, auth_name = self.wallet.read_alias(alias) + except BaseException, e: + # raise exception if verify fails (verify the chain) + if interactive: + self.show_message("Alias error: " + e.message) + return + + print target, signing_address, auth_name + + if auth_name is None: + a = self.wallet.aliases.get(alias) + if not a: + msg = "Warning: the alias '%s' is self-signed. The signing address is %s. Do you want to trust this alias?"%(alias,signing_address) + if interactive and self.question( msg ): + self.wallet.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address != a[0]: + 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 + if interactive and self.question( msg ): + self.wallet.aliases[alias] = (signing_address, target) + else: + target = None + else: + if signing_address not in self.wallet.authorities.keys(): + 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) + if interactive and self.question( msg ): + self.wallet.authorities[signing_address] = auth_name + else: + target = None + + if target: + self.wallet.aliases[alias] = (signing_address, target) + self.update_sending_tab() + + + return target + + + def do_send(self, w, data): payto_entry, label_entry, amount_entry, fee_entry = data - label = label_entry.get_text() + r = payto_entry.get_text() + r = r.strip() + + m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) + m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) + + if m1: + to_address = self.get_alias(r, interactive = True) + if not to_address: + return + elif m2: + to_address = m2.group(5) + else: + to_address = r - to_address = payto_entry.get_text() if not self.wallet.is_valid(to_address): - self.show_message( "invalid bitcoin address") + self.show_message( "invalid bitcoin address:\n"+to_address) return try: @@ -761,7 +902,7 @@ class BitcoinGUI: return if self.wallet.use_encryption: - password = password_dialog() + password = password_dialog(self.window) if not password: return else: @@ -782,7 +923,7 @@ class BitcoinGUI: label_entry.set_text("") amount_entry.set_text("") fee_entry.set_text("") - self.fee_box.hide() + #self.fee_box.hide() self.update_sending_tab() else: self.show_message( msg ) @@ -791,8 +932,20 @@ class BitcoinGUI: def treeview_button_press(self, treeview, event): if event.type == gtk.gdk._2BUTTON_PRESS: c = treeview.get_cursor()[0] - tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) - self.show_message(tx_details) + if treeview == self.history_treeview: + tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) + self.show_message(tx_details) + elif treeview == self.contacts_treeview: + m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) + a = self.wallet.aliases.get(m) + if a: + if a[0] in self.wallet.authorities.keys(): + s = self.wallet.authorities.get(a[0]) + else: + s = "self" + msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] + self.show_message(msg) + def treeview_key_press(self, treeview, event): c = treeview.get_cursor()[0] @@ -800,9 +953,21 @@ class BitcoinGUI: if c and c[0] == 0: treeview.parent.grab_focus() treeview.set_cursor((0,)) - elif event.keyval == gtk.keysyms.Return and treeview == self.history_treeview: - tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) - self.show_message(tx_details) + elif event.keyval == gtk.keysyms.Return: + if treeview == self.history_treeview: + tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) + self.show_message(tx_details) + elif treeview == self.contacts_treeview: + m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) + a = self.wallet.aliases.get(m) + if a: + if a[0] in self.wallet.authorities.keys(): + s = self.wallet.authorities.get(a[0]) + else: + s = "self" + msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] + self.show_message(msg) + return False def create_history_tab(self): @@ -827,7 +992,7 @@ class BitcoinGUI: tvcolumn.pack_start(cell, False) tvcolumn.add_attribute(cell, 'text', 2) - tvcolumn = gtk.TreeViewColumn('Label') + tvcolumn = gtk.TreeViewColumn('Description') treeview.append_column(tvcolumn) cell = gtk.CellRendererText() cell.set_property('foreground', 'grey') @@ -892,7 +1057,10 @@ class BitcoinGUI: liststore = self.recv_list if is_recv else self.addressbook_list treeview = gtk.TreeView(model= liststore) treeview.connect('key-press-event', self.treeview_key_press) + treeview.connect('button-press-event', self.treeview_button_press) treeview.show() + if not is_recv: + self.contacts_treeview = treeview tvcolumn = gtk.TreeViewColumn('Address') treeview.append_column(tvcolumn) @@ -993,7 +1161,7 @@ class BitcoinGUI: address = liststore.get_value( liststore.get_iter(path), 0) self.payto_entry.set_text( address ) self.notebook.set_current_page(1) - self.payto_amount_entry.grab_focus() + self.amount_entry.grab_focus() button.connect("clicked", payto, treeview, liststore) button.show() @@ -1013,7 +1181,7 @@ class BitcoinGUI: self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(self.wallet.interface.host, self.wallet.interface.blocks)) text = "Balance: %s "%( format_satoshis(c) ) - if u: text += "[+ %s unconfirmed]"%( format_satoshis(u) ) + if u: text += "[%s unconfirmed]"%( format_satoshis(u,True) ) if self.error: text = self.error self.status_bar.pop(self.context_id) self.status_bar.push(self.context_id, text) @@ -1033,6 +1201,11 @@ class BitcoinGUI: def update_sending_tab(self): # detect addresses that are not mine in history, add them here... self.addressbook_list.clear() + for alias, v in self.wallet.aliases.items(): + s, target = v + label = self.wallet.labels.get(alias) + self.addressbook_list.append((alias, label, '-')) + for address in self.wallet.addressbook: label = self.wallet.labels.get(address) n = 0 @@ -1062,16 +1235,23 @@ class BitcoinGUI: if is_default_label: label = tx['default_label'] tooltip = tx_hash + "\n%d confirmations"%conf - tx = self.wallet.tx_history.get(tx_hash) - details = "Transaction Details:\n\n" - details+= "Transaction ID:\n" + tx_hash + "\n\n" - details+= "Status: %d confirmations\n\n"%conf - details+= "Date: %s\n\n"%time_str - details+= "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" - details+= "Outputs:\n-"+ '\n-'.join(tx['outputs']) + # tx = self.wallet.tx_history.get(tx_hash) + details = "Transaction Details:\n\n" \ + + "Transaction ID:\n" + tx_hash + "\n\n" \ + + "Status: %d confirmations\n\n"%conf \ + + "Date: %s\n\n"%time_str \ + + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ + + "Outputs:\n-"+ '\n-'.join(tx['outputs']) + r = self.wallet.receipts.get(tx_hash) + if r: + details += "\n_______________________________________" \ + + '\n\nSigned URI: ' + r[2] \ + + "\n\nSigned by: " + r[0] \ + + '\n\nSignature: ' + r[1] + self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, - ('+' if v>0 else '') + format_satoshis(v), format_satoshis(balance), tooltip, details] ) + format_satoshis(v,True), format_satoshis(balance), tooltip, details] ) if cursor: self.history_treeview.set_cursor( cursor ) diff --git a/client/interface.py b/client/interface.py @@ -19,6 +19,9 @@ import random, socket, ast + + + class Interface: def __init__(self): self.servers = ['ecdsa.org','electrum.novit.ro'] # list of default servers diff --git a/client/msqr.py b/client/msqr.py @@ -0,0 +1,94 @@ +# from http://eli.thegreenplace.net/2009/03/07/computing-modular-square-roots-in-python/ + +def modular_sqrt(a, p): + """ Find a quadratic residue (mod p) of 'a'. p + must be an odd prime. + + Solve the congruence of the form: + x^2 = a (mod p) + And returns x. Note that p - x is also a root. + + 0 is returned is no square root exists for + these a and p. + + The Tonelli-Shanks algorithm is used (except + for some simple cases in which the solution + is known from an identity). This algorithm + runs in polynomial time (unless the + generalized Riemann hypothesis is false). + """ + # Simple cases + # + if legendre_symbol(a, p) != 1: + return 0 + elif a == 0: + return 0 + elif p == 2: + return p + elif p % 4 == 3: + return pow(a, (p + 1) / 4, p) + + # Partition p-1 to s * 2^e for an odd s (i.e. + # reduce all the powers of 2 from p-1) + # + s = p - 1 + e = 0 + while s % 2 == 0: + s /= 2 + e += 1 + + # Find some 'n' with a legendre symbol n|p = -1. + # Shouldn't take long. + # + n = 2 + while legendre_symbol(n, p) != -1: + n += 1 + + # Here be dragons! + # Read the paper "Square roots from 1; 24, 51, + # 10 to Dan Shanks" by Ezra Brown for more + # information + # + + # x is a guess of the square root that gets better + # with each iteration. + # b is the "fudge factor" - by how much we're off + # with the guess. The invariant x^2 = ab (mod p) + # is maintained throughout the loop. + # g is used for successive powers of n to update + # both a and b + # r is the exponent - decreases with each update + # + x = pow(a, (s + 1) / 2, p) + b = pow(a, s, p) + g = pow(n, s, p) + r = e + + while True: + t = b + m = 0 + for m in xrange(r): + if t == 1: + break + t = pow(t, 2, p) + + if m == 0: + return x + + gs = pow(g, 2 ** (r - m - 1), p) + g = (gs * gs) % p + x = (x * gs) % p + b = (b * g) % p + r = m + +def legendre_symbol(a, p): + """ Compute the Legendre symbol a|p using + Euler's criterion. p is a prime, a is + relatively prime to p (if p divides + a, then a|p = 0) + + Returns 1 if a has a square root modulo + p, -1 otherwise. + """ + ls = pow(a, (p - 1) / 2, p) + return -1 if ls == p - 1 else ls diff --git a/client/version.py b/client/version.py @@ -1,2 +1,2 @@ -ELECTRUM_VERSION = "0.37" +ELECTRUM_VERSION = "0.38" SEED_VERSION = 4 # bump this everytime the seed generation is modified diff --git a/client/wallet.py b/client/wallet.py @@ -17,8 +17,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys, base64, os, re, hashlib, socket, getpass, copy, operator, ast, random -from decimal import Decimal +import sys, base64, os, re, hashlib, copy, operator, ast try: import ecdsa @@ -151,10 +150,6 @@ def int_to_hex(i, length=1): return s.decode('hex')[::-1].encode('hex') -# URL decode -_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) -urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) - # AES EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s)) DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e)) @@ -218,9 +213,19 @@ def raw_tx( inputs, outputs, for_sig = None ): -from version import ELECTRUM_VERSION, SEED_VERSION +def format_satoshis(x, is_diff=False): + from decimal import Decimal + s = str( Decimal(x) /100000000 ) + if is_diff and x>0: + s = "+" + s + if not '.' in s: s += '.' + p = s.find('.') + s += " "*( 9 - ( len(s) - p )) + s = " "*( 5 - ( p )) + s + return s +from version import ELECTRUM_VERSION, SEED_VERSION @@ -242,6 +247,11 @@ class Wallet: self.status = {} # current status of addresses self.history = {} self.labels = {} # labels for addresses and transactions + self.aliases = {} # aliases for addresses + self.authorities = {} # trusted addresses + + self.receipts = {} # signed URIs + self.receipt = None # next receipt self.addressbook = [] # outgoing addresses, for payments # not saved @@ -287,7 +297,7 @@ class Wallet: seed = "%032x"%ecdsa.util.randrange( pow(2,128) ) self.init_mpk(seed) # encrypt - self.seed = wallet.pw_encode( seed, password ) + self.seed = self.pw_encode( seed, password ) def init_mpk(self,seed): # public key @@ -323,7 +333,7 @@ class Wallet: def get_sequence(self,n,for_change): return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key ) ) - def get_private_key2(self, address, password): + def get_private_key(self, address, password): """ Privatekey(type,n) = Master_private_key + H(n|S|type) """ order = generator_secp256k1.order() @@ -346,10 +356,65 @@ class Wallet: pk = number_to_string(secexp,order) return pk - - - def create_new_address2(self, for_change): + def msg_magic(self, message): + return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message + + def sign_message(self, address, message, password): + private_key = ecdsa.SigningKey.from_string( self.get_private_key(address, password), curve = SECP256k1 ) + public_key = private_key.get_verifying_key() + signature = private_key.sign_digest( Hash( self.msg_magic( message ) ), sigencode = ecdsa.util.sigencode_string ) + assert public_key.verify_digest( signature, Hash( self.msg_magic( message ) ), sigdecode = ecdsa.util.sigdecode_string) + for i in range(4): + sig = base64.b64encode( chr(27+i) + signature ) + try: + self.verify_message( address, sig, message) + return sig + except: + continue + else: + raise BaseException("error: cannot sign message") + + + def verify_message(self, address, signature, message): + """ See http://www.secg.org/download/aid-780/sec1-v2.pdf for the math """ + from ecdsa import numbertheory, ellipticcurve, util + import msqr + curve = curve_secp256k1 + G = generator_secp256k1 + order = G.order() + # extract r,s from signature + sig = base64.b64decode(signature) + if len(sig) != 65: raise BaseException("Wrong encoding") + r,s = util.sigdecode_string(sig[1:], order) + recid = ord(sig[0]) - 27 + # 1.1 + x = r + (recid/2) * order + # 1.3 + alpha = ( x * x * x + curve.a() * x + curve.b() ) % curve.p() + beta = msqr.modular_sqrt(alpha, curve.p()) + y = beta if (beta - recid) % 2 == 0 else curve.p() - beta + # 1.4 the constructor checks that nR is at infinity + R = ellipticcurve.Point(curve, x, y, order) + # 1.5 compute e from message: + h = Hash( self.msg_magic( message ) ) + e = string_to_number(h) + minus_e = -e % order + # 1.6 compute Q = r^-1 (sR - eG) + inv_r = numbertheory.inverse_mod(r,order) + Q = inv_r * ( s * R + minus_e * G ) + public_key = ecdsa.VerifyingKey.from_public_point( Q, curve = SECP256k1 ) + # check that Q is the public key + public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string) + # check that we get the original signing address + addr = public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ) + # print addr + if address != addr: + print "bad signature" + raise BaseException("Bad signature") + + + def create_new_address(self, for_change): """ Publickey(type,n) = Master_public_key + H(n|S|type)*point """ curve = SECP256k1 n = len(self.change_addresses) if for_change else len(self.addresses) @@ -374,12 +439,12 @@ class Wallet: is_new = False while True: if self.change_addresses == []: - self.create_new_address2(True) + self.create_new_address(True) is_new = True continue a = self.change_addresses[-1] if self.history.get(a): - self.create_new_address2(True) + self.create_new_address(True) is_new = True else: break @@ -387,13 +452,13 @@ class Wallet: n = self.gap_limit while True: if len(self.addresses) < n: - self.create_new_address2(False) + self.create_new_address(False) is_new = True continue if map( lambda a: self.history.get(a), self.addresses[-n:] ) == n*[[]]: break else: - self.create_new_address2(False) + self.create_new_address(False) is_new = True @@ -427,6 +492,9 @@ class Wallet: 'labels':self.labels, 'contacts':self.addressbook, 'imported_keys':self.imported_keys, + 'aliases':self.aliases, + 'authorities':self.authorities, + 'receipts':self.receipts, } f = open(self.path,"w") f.write( repr(s) ) @@ -457,6 +525,9 @@ class Wallet: self.labels = d.get('labels') self.addressbook = d.get('contacts') self.imported_keys = d.get('imported_keys',{}) + self.aliases = d.get('aliases',{}) + self.authorities = d.get('authorities',{}) + self.receipts = d.get('receipts',{}) except: raise BaseException(upgrade_msg) @@ -473,7 +544,7 @@ class Wallet: if not self.history.get(addr): n = n + 1 if n < self.gap_limit: - new_address = self.create_new_address2(False) + new_address = self.create_new_address(False) self.history[new_address] = [] #get from server return True, new_address else: @@ -559,7 +630,7 @@ class Wallet: s_inputs = [] for i in range(len(inputs)): addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i] - private_key = ecdsa.SigningKey.from_string( self.get_private_key2(addr, password), curve = SECP256k1 ) + private_key = ecdsa.SigningKey.from_string( self.get_private_key(addr, password), curve = SECP256k1 ) public_key = private_key.get_verifying_key() pubkey = public_key.to_string() tx = filter( raw_tx( inputs, outputs, for_sig = i ) ) @@ -634,19 +705,19 @@ class Wallet: def mktx(self, to_address, amount, label, password, fee=None): if not self.is_valid(to_address): raise BaseException("Invalid address") - inputs, total, fee = wallet.choose_tx_inputs( amount, fee ) + inputs, total, fee = self.choose_tx_inputs( amount, fee ) if not inputs: raise BaseException("Not enough funds") - outputs = wallet.choose_tx_outputs( to_address, amount, fee, total ) - s_inputs = wallet.sign_inputs( inputs, outputs, password ) + outputs = self.choose_tx_outputs( to_address, amount, fee, total ) + s_inputs = self.sign_inputs( inputs, outputs, password ) tx = filter( raw_tx( s_inputs, outputs ) ) if to_address not in self.addressbook: self.addressbook.append(to_address) if label: tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex') - wallet.labels[tx_hash] = label - wallet.save() + self.labels[tx_hash] = label + self.save() return tx def sendtx(self, tx): @@ -654,5 +725,51 @@ class Wallet: out = self.interface.send_tx(tx) if out != tx_hash: return False, "error: " + out + if self.receipt: + self.receipts[tx_hash] = self.receipt + self.receipt = None return True, out + + def read_alias(self, alias): + # this might not be the right place for this function. + import urllib + + m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias) + m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias) + if m1: + url = 'http://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) + elif m2: + url = 'http://' + alias + '/bitcoin.id' + else: + return '' + try: + lines = urllib.urlopen(url).readlines() + except: + return '' + + # line 0 + line = lines[0].strip().split(':') + if len(line) == 1: + auth_name = None + target = signing_addr = line[0] + else: + target, auth_name, signing_addr, signature = line + msg = "alias:%s:%s:%s"%(alias,target,auth_name) + print msg, signature + self.verify_message(signing_addr, signature, msg) + + # other lines are signed updates + for line in lines[1:]: + line = line.strip() + if not line: continue + line = line.split(':') + previous = target + print repr(line) + target, signature = line + self.verify_message(previous, signature, "alias:%s:%s"%(alias,target)) + + if not self.is_valid(target): + raise BaseException("Invalid bitcoin address") + + return target, signing_addr, auth_name