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/>.
19 # use this because this file is named gtk.py
20 from __future__ import absolute_import
23 import thread, time, ast, sys, re
24 import socket, traceback
28 from decimal import Decimal
29 from electrum.util import print_error
30 from electrum.bitcoin import is_valid
31 from electrum import mnemonic, pyqrnative, WalletStorage, Wallet
33 gtk.gdk.threads_init()
36 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
38 from electrum.util import format_satoshis
39 from electrum.network import DEFAULT_SERVERS
40 from electrum.bitcoin import MIN_RELAY_TX_FEE
42 def numbify(entry, is_int = False):
43 text = entry.get_text().strip()
45 if not is_int: chars +='.'
46 s = ''.join([i for i in text if i in chars])
51 s = s[:p] + '.' + s[p:p+8]
53 amount = int( Decimal(s) * 100000000 )
67 def show_seed_dialog(wallet, password, parent):
69 show_message("No seed")
72 seed = wallet.decode_seed(password)
74 show_message("Incorrect password")
76 dialog = gtk.MessageDialog(
78 flags = gtk.DIALOG_MODAL,
79 buttons = gtk.BUTTONS_OK,
80 message_format = "Your wallet generation seed is:\n\n" + seed \
81 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
82 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
83 dialog.set_title("Seed")
88 def restore_create_dialog():
90 # ask if the user wants to create a new wallet, or recover from a seed.
91 # if he wants to recover, and nothing is found, do not create wallet
92 dialog = gtk.Dialog("electrum", parent=None,
93 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
94 buttons= ("create", 0, "restore",1, "cancel",2) )
96 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
98 dialog.vbox.pack_start(label)
103 if r==2: return False
104 return 'restore' if r==1 else 'create'
108 def run_recovery_dialog():
109 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
110 dialog = gtk.MessageDialog(
112 flags = gtk.DIALOG_MODAL,
113 buttons = gtk.BUTTONS_OK_CANCEL,
114 message_format = message)
117 dialog.set_default_response(gtk.RESPONSE_OK)
119 # ask seed, server and gap in the same dialog
120 seed_box = gtk.HBox()
121 seed_label = gtk.Label('Seed or mnemonic:')
122 seed_label.set_size_request(150,-1)
123 seed_box.pack_start(seed_label, False, False, 10)
125 seed_entry = gtk.Entry()
127 seed_entry.set_size_request(450,-1)
128 seed_box.pack_start(seed_entry, False, False, 10)
129 add_help_button(seed_box, '.')
131 vbox.pack_start(seed_box, False, False, 5)
135 seed = seed_entry.get_text()
138 if r==gtk.RESPONSE_CANCEL:
144 print_error("Warning: Not hex, trying decode")
145 seed = mnemonic.mn_decode( seed.split(' ') )
147 show_message("no seed")
154 def run_settings_dialog(wallet, parent):
156 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
158 dialog = gtk.MessageDialog(
160 flags = gtk.DIALOG_MODAL,
161 buttons = gtk.BUTTONS_OK_CANCEL,
162 message_format = message)
165 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
167 dialog.set_image(image)
168 dialog.set_title("Settings")
171 dialog.set_default_response(gtk.RESPONSE_OK)
174 fee_entry = gtk.Entry()
175 fee_label = gtk.Label('Transaction fee:')
176 fee_label.set_size_request(150,10)
178 fee.pack_start(fee_label,False, False, 10)
179 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
180 fee_entry.connect('changed', numbify, False)
182 fee.pack_start(fee_entry,False,False, 10)
183 add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001')
185 vbox.pack_start(fee, False,False, 5)
188 nz_entry = gtk.Entry()
189 nz_label = gtk.Label('Display zeros:')
190 nz_label.set_size_request(150,10)
192 nz.pack_start(nz_label,False, False, 10)
193 nz_entry.set_text( str( wallet.num_zeros ))
194 nz_entry.connect('changed', numbify, True)
196 nz.pack_start(nz_entry,False,False, 10)
197 add_help_button(nz, "Number of zeros displayed after the decimal point.\nFor example, if this number is 2, then '5.' is displayed as '5.00'")
199 vbox.pack_start(nz, False,False, 5)
203 gui_label = gtk.Label('Default GUI:')
204 gui_label.set_size_request(150,10)
206 gui_box.pack_start(gui_label,False, False, 10)
207 gui_combo = gtk.combo_box_new_text()
208 gui_names = ['lite', 'classic', 'gtk', 'text']
209 for name in gui_names: gui_combo.append_text(name.capitalize())
211 gui_box.pack_start(gui_combo,False, False, 10)
212 gui_combo.set_active( gui_names.index( wallet.config.get("gui","lite")) )
214 add_help_button(gui_box, "Select which GUI mode to use at start up.")
216 vbox.pack_start(gui_box, False,False, 5)
220 fee = fee_entry.get_text()
221 nz = nz_entry.get_text()
222 gui = gui_names[ gui_combo.get_active()]
225 if r==gtk.RESPONSE_CANCEL:
229 fee = int( 100000000 * Decimal(fee) )
231 show_message("error")
239 show_message("error")
241 if wallet.num_zeros != nz:
242 wallet.num_zeros = nz
245 wallet.config.set_key('gui',gui,True)
250 def run_network_dialog( network, parent ):
252 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
253 interface = network.interface
255 if interface.is_connected:
256 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, network.blockchain.height)
258 status = "Not connected"
261 status = "Please choose a server.\nSelect cancel if you are offline."
263 server = interface.server
264 servers = network.get_servers()
266 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
267 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
268 dialog.set_title("Server")
269 dialog.set_image(image)
273 host_box = gtk.HBox()
274 host_label = gtk.Label('Connect to:')
275 host_label.set_size_request(100,-1)
277 host_box.pack_start(host_label, False, False, 10)
278 host_entry = gtk.Entry()
279 host_entry.set_size_request(200,-1)
280 host_entry.set_text(server)
282 host_box.pack_start(host_entry, False, False, 10)
283 add_help_button(host_box, 'The name and port number of your Electrum server, separated by a colon. Example: "ecdsa.org:50000". If no port number is provided, port 50000 will be tried. Some servers allow you to connect through http (port 80) or https (port 443)')
287 p_box = gtk.HBox(False, 10)
290 p_label = gtk.Label('Protocol:')
291 p_label.set_size_request(100,-1)
293 p_box.pack_start(p_label, False, False, 10)
295 radio1 = gtk.RadioButton(None, "tcp")
296 p_box.pack_start(radio1, True, True, 0)
298 radio2 = gtk.RadioButton(radio1, "http")
299 p_box.pack_start(radio2, True, True, 0)
303 return unicode(host_entry.get_text()).split(':')
305 def set_button(protocol):
308 elif protocol == 'h':
311 def set_protocol(protocol):
312 host = current_line()[0]
314 if protocol not in pp.keys():
315 protocol = pp.keys()[0]
318 host_entry.set_text( host + ':' + port + ':' + protocol)
320 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
321 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
323 server_list = gtk.ListStore(str)
324 for host in servers.keys():
325 server_list.append([host])
327 treeview = gtk.TreeView(model=server_list)
330 if interface.servers:
331 label = 'Active Servers'
333 label = 'Default Servers'
335 tvcolumn = gtk.TreeViewColumn(label)
336 treeview.append_column(tvcolumn)
337 cell = gtk.CellRendererText()
338 tvcolumn.pack_start(cell, False)
339 tvcolumn.add_attribute(cell, 'text', 0)
341 scroll = gtk.ScrolledWindow()
342 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
346 vbox.pack_start(host_box, False,False, 5)
347 vbox.pack_start(p_box, True, True, 0)
348 vbox.pack_start(scroll)
350 def my_treeview_cb(treeview):
351 path, view_column = treeview.get_cursor()
352 host = server_list.get_value( server_list.get_iter(path), 0)
358 protocol = pp.keys()[0]
360 host_entry.set_text( host + ':' + port + ':' + protocol)
363 treeview.connect('cursor-changed', my_treeview_cb)
367 server = host_entry.get_text()
370 if r==gtk.RESPONSE_CANCEL:
374 interface.set_server(server)
376 show_message("error:" + server)
380 wallet.config.set_key("server", server, True)
385 def show_message(message, parent=None):
386 dialog = gtk.MessageDialog(
388 flags = gtk.DIALOG_MODAL,
389 buttons = gtk.BUTTONS_CLOSE,
390 message_format = message )
395 def password_line(label):
396 password = gtk.HBox()
397 password_label = gtk.Label(label)
398 password_label.set_size_request(120,10)
399 password_label.show()
400 password.pack_start(password_label,False, False, 10)
401 password_entry = gtk.Entry()
402 password_entry.set_size_request(300,-1)
403 password_entry.set_visibility(False)
404 password_entry.show()
405 password.pack_start(password_entry,False,False, 10)
407 return password, password_entry
409 def password_dialog(parent):
410 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
411 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
412 dialog.get_image().set_visible(False)
413 current_pw, current_pw_entry = password_line('Password:')
414 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
415 dialog.vbox.pack_start(current_pw, False, True, 0)
417 result = dialog.run()
418 pw = current_pw_entry.get_text()
420 if result != gtk.RESPONSE_CANCEL: return pw
422 def change_password_dialog(wallet, parent, icon):
424 show_message("No seed")
428 msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted'
430 msg = "Please choose a password to encrypt your wallet keys"
432 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
433 dialog.set_title("Change password")
435 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
437 dialog.set_image(image)
439 if wallet.use_encryption:
440 current_pw, current_pw_entry = password_line('Current password:')
441 dialog.vbox.pack_start(current_pw, False, True, 0)
443 password, password_entry = password_line('New password:')
444 dialog.vbox.pack_start(password, False, True, 5)
445 password2, password2_entry = password_line('Confirm password:')
446 dialog.vbox.pack_start(password2, False, True, 5)
449 result = dialog.run()
450 password = current_pw_entry.get_text() if wallet.use_encryption else None
451 new_password = password_entry.get_text()
452 new_password2 = password2_entry.get_text()
454 if result == gtk.RESPONSE_CANCEL:
458 seed = wallet.decode_seed(password)
460 show_message("Incorrect password")
463 if new_password != new_password2:
464 show_message("passwords do not match")
467 wallet.update_password(seed, password, new_password)
470 if wallet.use_encryption:
471 icon.set_tooltip_text('wallet is encrypted')
473 icon.set_tooltip_text('wallet is unencrypted')
476 def add_help_button(hbox, message):
477 button = gtk.Button('?')
478 button.connect("clicked", lambda x: show_message(message))
480 hbox.pack_start(button,False, False)
483 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
485 gobject.type_register(MyWindow)
486 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
487 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
490 class ElectrumWindow:
492 def show_message(self, msg):
493 show_message(msg, self.window)
495 def __init__(self, wallet, config, network):
498 self.network = network
499 self.funds_error = False # True if not enough funds
500 self.num_zeros = int(self.config.get('num_zeros',0))
502 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
503 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
504 if not self.wallet.seed: title += ' [seedless]'
505 self.window.set_title(title)
506 self.window.connect("destroy", gtk.main_quit)
507 self.window.set_border_width(0)
508 self.window.connect('mykeypress', gtk.main_quit)
509 self.window.set_default_size(720, 350)
510 self.wallet_updated = False
514 self.notebook = gtk.Notebook()
515 self.create_history_tab()
517 self.create_send_tab()
518 self.create_recv_tab()
519 self.create_book_tab()
520 self.create_about_tab()
522 vbox.pack_start(self.notebook, True, True, 2)
524 self.status_bar = gtk.Statusbar()
525 vbox.pack_start(self.status_bar, False, False, 0)
527 self.status_image = gtk.Image()
528 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
529 self.status_image.set_alignment(True, 0.5 )
530 self.status_image.show()
532 self.network_button = gtk.Button()
533 self.network_button.connect("clicked", lambda x: run_network_dialog(self.network, self.window) )
534 self.network_button.add(self.status_image)
535 self.network_button.set_relief(gtk.RELIEF_NONE)
536 self.network_button.show()
537 self.status_bar.pack_end(self.network_button, False, False)
540 def seedb(w, wallet):
541 if wallet.use_encryption:
542 password = password_dialog(self.window)
543 if not password: return
544 else: password = None
545 show_seed_dialog(wallet, password, self.window)
546 button = gtk.Button('S')
547 button.connect("clicked", seedb, wallet )
548 button.set_relief(gtk.RELIEF_NONE)
550 self.status_bar.pack_end(button,False, False)
552 settings_icon = gtk.Image()
553 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
554 settings_icon.set_alignment(0.5, 0.5)
555 settings_icon.set_size_request(16,16 )
558 prefs_button = gtk.Button()
559 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
560 prefs_button.add(settings_icon)
561 prefs_button.set_tooltip_text("Settings")
562 prefs_button.set_relief(gtk.RELIEF_NONE)
564 self.status_bar.pack_end(prefs_button,False,False)
566 pw_icon = gtk.Image()
567 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
568 pw_icon.set_alignment(0.5, 0.5)
569 pw_icon.set_size_request(16,16 )
573 password_button = gtk.Button()
574 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
575 password_button.add(pw_icon)
576 password_button.set_relief(gtk.RELIEF_NONE)
577 password_button.show()
578 self.status_bar.pack_end(password_button,False,False)
580 self.window.add(vbox)
581 self.window.show_all()
584 self.context_id = self.status_bar.get_context_id("statusbar")
585 self.update_status_bar()
587 self.network.register_callback('updated', self.update_callback)
590 def update_status_bar_thread():
592 gobject.idle_add( self.update_status_bar )
596 def check_recipient_thread():
600 if self.payto_entry.is_focus():
602 r = self.payto_entry.get_text()
606 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
608 to_address = self.wallet.get_alias(r, interactive=False)
612 s = r + ' <' + to_address + '>'
613 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
616 thread.start_new_thread(update_status_bar_thread, ())
618 thread.start_new_thread(check_recipient_thread, ())
619 self.notebook.set_current_page(0)
621 def update_callback(self):
622 self.wallet_updated = True
625 def add_tab(self, page, name):
626 tab_label = gtk.Label(name)
628 self.notebook.append_page(page, tab_label)
631 def create_send_tab(self):
633 page = vbox = gtk.VBox()
637 payto_label = gtk.Label('Pay to:')
638 payto_label.set_size_request(100,-1)
639 payto.pack_start(payto_label, False)
640 payto_entry = gtk.Entry()
641 payto_entry.set_size_request(450, 26)
642 payto.pack_start(payto_entry, False)
643 vbox.pack_start(payto, False, False, 5)
646 message_label = gtk.Label('Description:')
647 message_label.set_size_request(100,-1)
648 message.pack_start(message_label, False)
649 message_entry = gtk.Entry()
650 message_entry.set_size_request(450, 26)
651 message.pack_start(message_entry, False)
652 vbox.pack_start(message, False, False, 5)
654 amount_box = gtk.HBox()
655 amount_label = gtk.Label('Amount:')
656 amount_label.set_size_request(100,-1)
657 amount_box.pack_start(amount_label, False)
658 amount_entry = gtk.Entry()
659 amount_entry.set_size_request(120, -1)
660 amount_box.pack_start(amount_entry, False)
661 vbox.pack_start(amount_box, False, False, 5)
663 self.fee_box = fee_box = gtk.HBox()
664 fee_label = gtk.Label('Fee:')
665 fee_label.set_size_request(100,-1)
666 fee_box.pack_start(fee_label, False)
667 fee_entry = gtk.Entry()
668 fee_entry.set_size_request(60, 26)
669 fee_box.pack_start(fee_entry, False)
670 vbox.pack_start(fee_box, False, False, 5)
673 empty_label = gtk.Label('')
674 empty_label.set_size_request(100,-1)
675 end_box.pack_start(empty_label, False)
676 send_button = gtk.Button("Send")
678 end_box.pack_start(send_button, False, False, 0)
679 clear_button = gtk.Button("Clear")
681 end_box.pack_start(clear_button, False, False, 15)
682 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
683 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
685 vbox.pack_start(end_box, False, False, 5)
687 # display this line only if there is a signature
688 payto_sig = gtk.HBox()
689 payto_sig_id = gtk.Label('')
690 payto_sig.pack_start(payto_sig_id, False)
691 vbox.pack_start(payto_sig, True, True, 5)
694 self.user_fee = False
696 def entry_changed( entry, is_fee ):
697 self.funds_error = False
698 amount = numbify(amount_entry)
699 fee = numbify(fee_entry)
700 if not is_fee: fee = None
703 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
705 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
708 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
709 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
710 send_button.set_sensitive(True)
712 send_button.set_sensitive(False)
713 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
714 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
715 self.funds_error = True
717 amount_entry.connect('changed', entry_changed, False)
718 fee_entry.connect('changed', entry_changed, True)
720 self.payto_entry = payto_entry
721 self.payto_fee_entry = fee_entry
722 self.payto_sig_id = payto_sig_id
723 self.payto_sig = payto_sig
724 self.amount_entry = amount_entry
725 self.message_entry = message_entry
726 self.add_tab(page, 'Send')
728 def set_frozen(self,entry,frozen):
730 entry.set_editable(False)
731 entry.set_has_frame(False)
732 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
734 entry.set_editable(True)
735 entry.set_has_frame(True)
736 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
738 def set_url(self, url):
739 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
740 self.notebook.set_current_page(1)
741 self.payto_entry.set_text(payto)
742 self.message_entry.set_text(message)
743 self.amount_entry.set_text(amount)
745 self.set_frozen(self.payto_entry,True)
746 self.set_frozen(self.amount_entry,True)
747 self.set_frozen(self.message_entry,True)
748 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
750 self.payto_sig.set_visible(False)
752 def create_about_tab(self):
757 tv.set_editable(False)
758 tv.set_cursor_visible(False)
759 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
760 scroll = gtk.ScrolledWindow()
762 page.pack_start(scroll)
763 self.info = tv.get_buffer()
764 self.add_tab(page, 'Wall')
766 def do_clear(self, w, data):
767 self.payto_sig.set_visible(False)
768 self.payto_fee_entry.set_text('')
769 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
770 self.set_frozen(entry,False)
773 def question(self,msg):
774 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
776 result = dialog.run()
778 return result == gtk.RESPONSE_OK
780 def do_send(self, w, data):
781 payto_entry, label_entry, amount_entry, fee_entry = data
782 label = label_entry.get_text()
783 r = payto_entry.get_text()
786 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
787 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
790 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
794 self.update_sending_tab()
797 to_address = m2.group(5)
801 if not is_valid(to_address):
802 self.show_message( "invalid bitcoin address:\n"+to_address)
806 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
808 self.show_message( "invalid amount")
811 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
813 self.show_message( "invalid fee")
816 if self.wallet.use_encryption:
817 password = password_dialog(self.window)
824 tx = self.wallet.mktx( [(to_address, amount)], password, fee )
825 except BaseException, e:
826 self.show_message(str(e))
829 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
830 self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." )
835 self.wallet.labels[tx.hash()] = label
837 status, msg = self.wallet.sendtx( tx )
839 self.show_message( "payment sent.\n" + msg )
840 payto_entry.set_text("")
841 label_entry.set_text("")
842 amount_entry.set_text("")
843 fee_entry.set_text("")
845 self.update_sending_tab()
847 self.show_message( msg )
850 def treeview_button_press(self, treeview, event):
851 if event.type == gtk.gdk._2BUTTON_PRESS:
852 c = treeview.get_cursor()[0]
853 if treeview == self.history_treeview:
854 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
855 self.show_message(tx_details)
856 elif treeview == self.contacts_treeview:
857 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
858 #a = self.wallet.aliases.get(m)
860 # if a[0] in self.wallet.authorities.keys():
861 # s = self.wallet.authorities.get(a[0])
864 # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
865 # self.show_message(msg)
868 def treeview_key_press(self, treeview, event):
869 c = treeview.get_cursor()[0]
870 if event.keyval == gtk.keysyms.Up:
872 treeview.parent.grab_focus()
873 treeview.set_cursor((0,))
874 elif event.keyval == gtk.keysyms.Return:
875 if treeview == self.history_treeview:
876 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
877 self.show_message(tx_details)
878 elif treeview == self.contacts_treeview:
879 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
880 #a = self.wallet.aliases.get(m)
882 # if a[0] in self.wallet.authorities.keys():
883 # s = self.wallet.authorities.get(a[0])
886 # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
887 # self.show_message(msg)
891 def create_history_tab(self):
893 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
894 treeview = gtk.TreeView(model=self.history_list)
895 self.history_treeview = treeview
896 treeview.set_tooltip_column(7)
898 treeview.connect('key-press-event', self.treeview_key_press)
899 treeview.connect('button-press-event', self.treeview_button_press)
901 tvcolumn = gtk.TreeViewColumn('')
902 treeview.append_column(tvcolumn)
903 cell = gtk.CellRendererPixbuf()
904 tvcolumn.pack_start(cell, False)
905 tvcolumn.set_attributes(cell, stock_id=1)
907 tvcolumn = gtk.TreeViewColumn('Date')
908 treeview.append_column(tvcolumn)
909 cell = gtk.CellRendererText()
910 tvcolumn.pack_start(cell, False)
911 tvcolumn.add_attribute(cell, 'text', 2)
913 tvcolumn = gtk.TreeViewColumn('Description')
914 treeview.append_column(tvcolumn)
915 cell = gtk.CellRendererText()
916 cell.set_property('foreground', 'grey')
917 cell.set_property('family', MONOSPACE_FONT)
918 cell.set_property('editable', True)
919 def edited_cb(cell, path, new_text, h_list):
920 tx = h_list.get_value( h_list.get_iter(path), 0)
921 self.wallet.labels[tx] = new_text
923 self.update_history_tab()
924 cell.connect('edited', edited_cb, self.history_list)
925 def editing_started(cell, entry, path, h_list):
926 tx = h_list.get_value( h_list.get_iter(path), 0)
927 if not self.wallet.labels.get(tx): entry.set_text('')
928 cell.connect('editing-started', editing_started, self.history_list)
929 tvcolumn.set_expand(True)
930 tvcolumn.pack_start(cell, True)
931 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
933 tvcolumn = gtk.TreeViewColumn('Amount')
934 treeview.append_column(tvcolumn)
935 cell = gtk.CellRendererText()
936 cell.set_alignment(1, 0.5)
937 cell.set_property('family', MONOSPACE_FONT)
938 tvcolumn.pack_start(cell, False)
939 tvcolumn.add_attribute(cell, 'text', 5)
941 tvcolumn = gtk.TreeViewColumn('Balance')
942 treeview.append_column(tvcolumn)
943 cell = gtk.CellRendererText()
944 cell.set_alignment(1, 0.5)
945 cell.set_property('family', MONOSPACE_FONT)
946 tvcolumn.pack_start(cell, False)
947 tvcolumn.add_attribute(cell, 'text', 6)
949 tvcolumn = gtk.TreeViewColumn('Tooltip')
950 treeview.append_column(tvcolumn)
951 cell = gtk.CellRendererText()
952 tvcolumn.pack_start(cell, False)
953 tvcolumn.add_attribute(cell, 'text', 7)
954 tvcolumn.set_visible(False)
956 scroll = gtk.ScrolledWindow()
957 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
960 self.add_tab(scroll, 'History')
961 self.update_history_tab()
964 def create_recv_tab(self):
965 self.recv_list = gtk.ListStore(str, str, str)
966 self.add_tab( self.make_address_list(True), 'Receive')
967 self.update_receiving_tab()
969 def create_book_tab(self):
970 self.addressbook_list = gtk.ListStore(str, str, str)
971 self.add_tab( self.make_address_list(False), 'Contacts')
972 self.update_sending_tab()
974 def make_address_list(self, is_recv):
975 liststore = self.recv_list if is_recv else self.addressbook_list
976 treeview = gtk.TreeView(model= liststore)
977 treeview.connect('key-press-event', self.treeview_key_press)
978 treeview.connect('button-press-event', self.treeview_button_press)
981 self.contacts_treeview = treeview
983 tvcolumn = gtk.TreeViewColumn('Address')
984 treeview.append_column(tvcolumn)
985 cell = gtk.CellRendererText()
986 cell.set_property('family', MONOSPACE_FONT)
987 tvcolumn.pack_start(cell, True)
988 tvcolumn.add_attribute(cell, 'text', 0)
990 tvcolumn = gtk.TreeViewColumn('Label')
991 tvcolumn.set_expand(True)
992 treeview.append_column(tvcolumn)
993 cell = gtk.CellRendererText()
994 cell.set_property('editable', True)
995 def edited_cb2(cell, path, new_text, liststore):
996 address = liststore.get_value( liststore.get_iter(path), 0)
997 self.wallet.labels[address] = new_text
999 self.update_receiving_tab()
1000 self.update_sending_tab()
1001 self.update_history_tab()
1002 cell.connect('edited', edited_cb2, liststore)
1003 tvcolumn.pack_start(cell, True)
1004 tvcolumn.add_attribute(cell, 'text', 1)
1006 tvcolumn = gtk.TreeViewColumn('Tx')
1007 treeview.append_column(tvcolumn)
1008 cell = gtk.CellRendererText()
1009 tvcolumn.pack_start(cell, True)
1010 tvcolumn.add_attribute(cell, 'text', 2)
1012 scroll = gtk.ScrolledWindow()
1013 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1014 scroll.add(treeview)
1018 button = gtk.Button("New")
1019 button.connect("clicked", self.newaddress_dialog)
1021 hbox.pack_start(button,False)
1023 def showqrcode(w, treeview, liststore):
1024 path, col = treeview.get_cursor()
1026 address = liststore.get_value(liststore.get_iter(path), 0)
1027 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1031 size = qr.getModuleCount()*boxsize
1032 def area_expose_cb(area, event):
1033 style = area.get_style()
1034 k = qr.getModuleCount()
1037 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1038 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1039 area = gtk.DrawingArea()
1040 area.set_size_request(size, size)
1041 area.connect("expose-event", area_expose_cb)
1043 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1044 dialog.vbox.add(area)
1048 button = gtk.Button("QR")
1049 button.connect("clicked", showqrcode, treeview, liststore)
1051 hbox.pack_start(button,False)
1053 button = gtk.Button("Copy to clipboard")
1054 def copy2clipboard(w, treeview, liststore):
1056 path, col = treeview.get_cursor()
1058 address = liststore.get_value( liststore.get_iter(path), 0)
1059 if platform.system() == 'Windows':
1060 from Tkinter import Tk
1064 r.clipboard_append( address )
1067 c = gtk.clipboard_get()
1068 c.set_text( address )
1069 button.connect("clicked", copy2clipboard, treeview, liststore)
1071 hbox.pack_start(button,False)
1074 button = gtk.Button("Pay to")
1075 def payto(w, treeview, liststore):
1076 path, col = treeview.get_cursor()
1078 address = liststore.get_value( liststore.get_iter(path), 0)
1079 self.payto_entry.set_text( address )
1080 self.notebook.set_current_page(1)
1081 self.amount_entry.grab_focus()
1083 button.connect("clicked", payto, treeview, liststore)
1085 hbox.pack_start(button,False)
1088 vbox.pack_start(scroll,True)
1089 vbox.pack_start(hbox, False)
1092 def update_status_bar(self):
1093 interface = self.network.interface
1094 if self.funds_error:
1095 text = "Not enough funds"
1096 elif interface and interface.is_connected:
1097 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height))
1098 if not self.wallet.up_to_date:
1099 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1100 text = "Synchronizing..."
1102 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1103 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height))
1104 c, u = self.wallet.get_balance()
1105 text = "Balance: %s "%( format_satoshis(c,False,self.num_zeros) )
1106 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.num_zeros).strip() )
1108 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1109 self.network_button.set_tooltip_text("Not connected.")
1110 text = "Not connected"
1112 self.status_bar.pop(self.context_id)
1113 self.status_bar.push(self.context_id, text)
1115 if self.wallet.up_to_date and self.wallet_updated:
1116 self.update_history_tab()
1117 self.update_receiving_tab()
1118 # addressbook too...
1119 self.info.set_text( self.network.banner )
1120 self.wallet_updated = False
1122 def update_receiving_tab(self):
1123 self.recv_list.clear()
1124 for address in self.wallet.addresses(True):
1125 if self.wallet.is_change(address):continue
1126 label = self.wallet.labels.get(address)
1127 h = self.wallet.history.get(address,[])
1129 tx = "None" if n==0 else "%d"%n
1130 self.recv_list.append((address, label, tx ))
1132 def update_sending_tab(self):
1133 # detect addresses that are not mine in history, add them here...
1134 self.addressbook_list.clear()
1135 #for alias, v in self.wallet.aliases.items():
1137 # label = self.wallet.labels.get(alias)
1138 # self.addressbook_list.append((alias, label, '-'))
1140 for address in self.wallet.addressbook:
1141 label = self.wallet.labels.get(address)
1142 n = self.wallet.get_num_tx(address)
1143 self.addressbook_list.append((address, label, "%d"%n))
1145 def update_history_tab(self):
1146 cursor = self.history_treeview.get_cursor()[0]
1147 self.history_list.clear()
1149 for item in self.wallet.get_tx_history():
1150 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1153 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1156 conf_icon = gtk.STOCK_APPLY
1158 time_str = 'unverified'
1161 time_str = 'pending'
1162 conf_icon = gtk.STOCK_EXECUTE
1164 label, is_default_label = self.wallet.get_label(tx_hash)
1165 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1166 details = self.get_tx_details(tx_hash)
1168 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1169 format_satoshis(value,True,self.num_zeros, whitespaces=True),
1170 format_satoshis(balance,False,self.num_zeros, whitespaces=True), tooltip, details] )
1171 if cursor: self.history_treeview.set_cursor( cursor )
1174 def get_tx_details(self, tx_hash):
1176 if not tx_hash: return ''
1177 tx = self.wallet.transactions.get(tx_hash)
1178 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
1179 conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1182 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1184 time_str = 'pending'
1186 inputs = map(lambda x: x.get('address'), tx.inputs)
1187 outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1188 tx_details = "Transaction Details" +"\n\n" \
1189 + "Transaction ID:\n" + tx_hash + "\n\n" \
1190 + "Status: %d confirmations\n"%conf
1193 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1194 + "Transaction fee: %s\n"% format_satoshis(fee, False)
1196 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1197 + "Transaction fee: unknown\n"
1199 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1201 tx_details += "Date: %s\n\n"%time_str \
1202 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1203 + "Outputs:\n-"+ '\n-'.join(outputs)
1209 def newaddress_dialog(self, w):
1211 title = "New Contact"
1212 dialog = gtk.Dialog(title, parent=self.window,
1213 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1214 buttons= ("cancel", 0, "ok",1) )
1218 label_label = gtk.Label('Label:')
1219 label_label.set_size_request(120,10)
1221 label.pack_start(label_label)
1222 label_entry = gtk.Entry()
1224 label.pack_start(label_entry)
1226 dialog.vbox.pack_start(label, False, True, 5)
1228 address = gtk.HBox()
1229 address_label = gtk.Label('Address:')
1230 address_label.set_size_request(120,10)
1231 address_label.show()
1232 address.pack_start(address_label)
1233 address_entry = gtk.Entry()
1234 address_entry.show()
1235 address.pack_start(address_entry)
1237 dialog.vbox.pack_start(address, False, True, 5)
1239 result = dialog.run()
1240 address = address_entry.get_text()
1241 label = label_entry.get_text()
1245 if is_valid(address):
1246 self.wallet.add_contact(address,label)
1247 self.update_sending_tab()
1249 errorDialog = gtk.MessageDialog(
1251 flags=gtk.DIALOG_MODAL,
1252 buttons= gtk.BUTTONS_CLOSE,
1253 message_format = "Invalid address")
1256 errorDialog.destroy()
1260 class ElectrumGui():
1262 def __init__(self, config, network):
1263 self.network = network
1264 self.config = config
1267 def main(self, url=None):
1269 storage = WalletStorage(self.config)
1270 if not storage.file_exists:
1271 action = self.restore_or_create()
1274 self.wallet = wallet = Wallet(storage)
1275 gap = self.config.get('gap_limit', 5)
1277 wallet.gap_limit = gap
1278 wallet.storage.put('gap_limit', gap, True)
1280 self.wallet.start_threads(self.network)
1282 if action == 'create':
1283 wallet.init_seed(None)
1285 wallet.create_accounts()
1286 wallet.synchronize() # generate first addresses offline
1287 elif action == 'restore':
1288 seed = self.seed_dialog()
1289 wallet.init_seed(seed)
1291 self.restore_wallet(wallet)
1296 self.wallet = Wallet(storage)
1297 self.wallet.start_threads(self.network)
1299 w = ElectrumWindow(self.wallet, self.config, self.network)
1300 if url: w.set_url(url)
1303 def restore_or_create(self):
1304 return restore_create_dialog()
1306 def seed_dialog(self):
1307 return run_recovery_dialog()
1309 def verify_seed(self):
1310 self.wallet.save_seed()
1313 def network_dialog(self):
1314 return run_network_dialog( self.network, parent=None )
1316 def show_seed(self):
1317 show_seed_dialog(self.wallet, None, None)
1319 def password_dialog(self):
1320 change_password_dialog(self.wallet, None, None)
1322 def restore_wallet(self, wallet):
1324 dialog = gtk.MessageDialog(
1326 flags = gtk.DIALOG_MODAL,
1327 buttons = gtk.BUTTONS_CANCEL,
1328 message_format = "Please wait..." )
1331 def recover_thread( wallet, dialog ):
1332 wallet.restore(lambda x:x)
1333 gobject.idle_add( dialog.destroy )
1335 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1338 if r==gtk.RESPONSE_CANCEL: return False
1339 if not wallet.is_found():
1340 show_message("No transactions found for this seed")