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:
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