3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import thread, time, ast, sys, re
21 import socket, traceback
25 from decimal import Decimal
26 from util import print_error
28 import pyqrnative, mnemonic
30 gtk.gdk.threads_init()
33 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
35 from wallet import format_satoshis
36 from interface import DEFAULT_SERVERS
38 def numbify(entry, is_int = False):
39 text = entry.get_text().strip()
41 if not is_int: chars +='.'
42 s = ''.join([i for i in text if i in chars])
47 s = s[:p] + '.' + s[p:p+8]
49 amount = int( Decimal(s) * 100000000 )
63 def show_seed_dialog(wallet, password, parent):
65 show_message("No seed")
68 seed = wallet.pw_decode( wallet.seed, password)
70 show_message("Incorrect password")
72 dialog = gtk.MessageDialog(
74 flags = gtk.DIALOG_MODAL,
75 buttons = gtk.BUTTONS_OK,
76 message_format = "Your wallet generation seed is:\n\n" + seed \
77 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
78 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
79 dialog.set_title("Seed")
84 def restore_create_dialog(wallet):
86 # ask if the user wants to create a new wallet, or recover from a seed.
87 # if he wants to recover, and nothing is found, do not create wallet
88 dialog = gtk.Dialog("electrum", parent=None,
89 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
90 buttons= ("create", 0, "restore",1, "cancel",2) )
92 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
94 dialog.vbox.pack_start(label)
103 # ask for the server.
104 if not run_network_dialog( wallet, parent=None ): return False
108 wallet.new_seed(None)
110 wallet.init_mpk( wallet.seed )
111 wallet.up_to_date_event.clear()
114 # run a dialog indicating the seed, ask the user to remember it
115 show_seed_dialog(wallet, None, None)
118 change_password_dialog(wallet, None, None)
120 # ask for seed and gap.
121 run_recovery_dialog( wallet )
123 dialog = gtk.MessageDialog(
125 flags = gtk.DIALOG_MODAL,
126 buttons = gtk.BUTTONS_CANCEL,
127 message_format = "Please wait..." )
130 def recover_thread( wallet, dialog ):
131 wallet.init_mpk( wallet.seed ) # not encrypted at this point
132 wallet.up_to_date_event.clear()
135 if wallet.is_found():
136 # history and addressbook
137 wallet.update_tx_history()
138 wallet.fill_addressbook()
139 print "Recovery successful"
141 gobject.idle_add( dialog.destroy )
143 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
146 if r==gtk.RESPONSE_CANCEL: return False
147 if not wallet.is_found:
148 show_message("No transactions found for this seed")
154 def run_recovery_dialog(wallet):
155 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
156 dialog = gtk.MessageDialog(
158 flags = gtk.DIALOG_MODAL,
159 buttons = gtk.BUTTONS_OK_CANCEL,
160 message_format = message)
163 dialog.set_default_response(gtk.RESPONSE_OK)
165 # ask seed, server and gap in the same dialog
166 seed_box = gtk.HBox()
167 seed_label = gtk.Label('Seed or mnemonic:')
168 seed_label.set_size_request(150,-1)
169 seed_box.pack_start(seed_label, False, False, 10)
171 seed_entry = gtk.Entry()
173 seed_entry.set_size_request(450,-1)
174 seed_box.pack_start(seed_entry, False, False, 10)
175 add_help_button(seed_box, '.')
177 vbox.pack_start(seed_box, False, False, 5)
180 gap_label = gtk.Label('Gap limit:')
181 gap_label.set_size_request(150,10)
183 gap.pack_start(gap_label,False, False, 10)
184 gap_entry = gtk.Entry()
185 gap_entry.set_text("%d"%wallet.gap_limit)
186 gap_entry.connect('changed', numbify, True)
188 gap.pack_start(gap_entry,False,False, 10)
189 add_help_button(gap, 'The maximum gap that is allowed between unused addresses in your wallet. During wallet recovery, this parameter is used to decide when to stop the recovery process. If you increase this value, you will need to remember it in order to be able to recover your wallet from seed.')
191 vbox.pack_start(gap, False,False, 5)
195 gap = gap_entry.get_text()
196 seed = seed_entry.get_text()
199 if r==gtk.RESPONSE_CANCEL:
204 show_message("error")
210 print_error("Warning: Not hex, trying decode")
211 seed = mnemonic.mn_decode( seed.split(' ') )
213 show_message("no seed")
217 wallet.gap_limit = gap
222 def run_settings_dialog(wallet, parent):
224 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
226 dialog = gtk.MessageDialog(
228 flags = gtk.DIALOG_MODAL,
229 buttons = gtk.BUTTONS_OK_CANCEL,
230 message_format = message)
233 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
235 dialog.set_image(image)
236 dialog.set_title("Settings")
239 dialog.set_default_response(gtk.RESPONSE_OK)
242 fee_entry = gtk.Entry()
243 fee_label = gtk.Label('Transaction fee:')
244 fee_label.set_size_request(150,10)
246 fee.pack_start(fee_label,False, False, 10)
247 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
248 fee_entry.connect('changed', numbify, False)
250 fee.pack_start(fee_entry,False,False, 10)
251 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
253 vbox.pack_start(fee, False,False, 5)
256 nz_entry = gtk.Entry()
257 nz_label = gtk.Label('Display zeros:')
258 nz_label.set_size_request(150,10)
260 nz.pack_start(nz_label,False, False, 10)
261 nz_entry.set_text( str( wallet.num_zeros ))
262 nz_entry.connect('changed', numbify, True)
264 nz.pack_start(nz_entry,False,False, 10)
265 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'")
267 vbox.pack_start(nz, False,False, 5)
271 gui_label = gtk.Label('Default GUI:')
272 gui_label.set_size_request(150,10)
274 gui_box.pack_start(gui_label,False, False, 10)
275 gui_combo = gtk.combo_box_new_text()
276 gui_names = ['lite', 'classic', 'gtk', 'text']
277 for name in gui_names: gui_combo.append_text(name.capitalize())
279 gui_box.pack_start(gui_combo,False, False, 10)
280 gui_combo.set_active( gui_names.index( wallet.config.get("gui","lite")) )
282 add_help_button(gui_box, "Select which GUI mode to use at start up.")
284 vbox.pack_start(gui_box, False,False, 5)
288 fee = fee_entry.get_text()
289 nz = nz_entry.get_text()
290 gui = gui_names[ gui_combo.get_active()]
293 if r==gtk.RESPONSE_CANCEL:
297 fee = int( 100000000 * Decimal(fee) )
299 show_message("error")
301 if wallet.fee != fee:
309 show_message("error")
311 if wallet.num_zeros != nz:
312 wallet.num_zeros = nz
315 wallet.config.set_key('gui',gui,True)
320 def run_network_dialog( wallet, parent ):
322 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
323 interface = wallet.interface
325 if interface.is_connected:
326 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.verifier.height)
328 status = "Not connected"
331 status = "Please choose a server."
333 server = interface.server
335 if not wallet.interface.servers:
337 for x in DEFAULT_SERVERS:
338 h,port,protocol = x.split(':')
339 servers_list.append( (h,[(protocol,port)] ) )
341 servers_list = wallet.interface.servers
344 for item in servers_list:
348 protocol, port = item2
352 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
353 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
354 dialog.set_title("Server")
355 dialog.set_image(image)
359 host_box = gtk.HBox()
360 host_label = gtk.Label('Connect to:')
361 host_label.set_size_request(100,-1)
363 host_box.pack_start(host_label, False, False, 10)
364 host_entry = gtk.Entry()
365 host_entry.set_size_request(200,-1)
366 host_entry.set_text(server)
368 host_box.pack_start(host_entry, False, False, 10)
369 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)')
373 p_box = gtk.HBox(False, 10)
376 p_label = gtk.Label('Protocol:')
377 p_label.set_size_request(100,-1)
379 p_box.pack_start(p_label, False, False, 10)
381 radio1 = gtk.RadioButton(None, "tcp")
382 p_box.pack_start(radio1, True, True, 0)
384 radio2 = gtk.RadioButton(radio1, "http")
385 p_box.pack_start(radio2, True, True, 0)
389 return unicode(host_entry.get_text()).split(':')
391 def set_button(protocol):
394 elif protocol == 'h':
397 def set_protocol(protocol):
398 host = current_line()[0]
400 if protocol not in pp.keys():
401 protocol = pp.keys()[0]
404 host_entry.set_text( host + ':' + port + ':' + protocol)
406 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
407 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
409 server_list = gtk.ListStore(str)
410 for host in plist.keys():
411 server_list.append([host])
413 treeview = gtk.TreeView(model=server_list)
416 if wallet.interface.servers:
417 label = 'Active Servers'
419 label = 'Default Servers'
421 tvcolumn = gtk.TreeViewColumn(label)
422 treeview.append_column(tvcolumn)
423 cell = gtk.CellRendererText()
424 tvcolumn.pack_start(cell, False)
425 tvcolumn.add_attribute(cell, 'text', 0)
427 scroll = gtk.ScrolledWindow()
428 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
432 vbox.pack_start(host_box, False,False, 5)
433 vbox.pack_start(p_box, True, True, 0)
434 vbox.pack_start(scroll)
436 def my_treeview_cb(treeview):
437 path, view_column = treeview.get_cursor()
438 host = server_list.get_value( server_list.get_iter(path), 0)
444 protocol = pp.keys()[0]
446 host_entry.set_text( host + ':' + port + ':' + protocol)
449 treeview.connect('cursor-changed', my_treeview_cb)
453 server = host_entry.get_text()
456 if r==gtk.RESPONSE_CANCEL:
460 interface.set_server(server)
462 show_message("error:" + server)
466 wallet.config.set_key("server", server, True)
471 def show_message(message, parent=None):
472 dialog = gtk.MessageDialog(
474 flags = gtk.DIALOG_MODAL,
475 buttons = gtk.BUTTONS_CLOSE,
476 message_format = message )
481 def password_line(label):
482 password = gtk.HBox()
483 password_label = gtk.Label(label)
484 password_label.set_size_request(120,10)
485 password_label.show()
486 password.pack_start(password_label,False, False, 10)
487 password_entry = gtk.Entry()
488 password_entry.set_size_request(300,-1)
489 password_entry.set_visibility(False)
490 password_entry.show()
491 password.pack_start(password_entry,False,False, 10)
493 return password, password_entry
495 def password_dialog(parent):
496 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
497 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
498 dialog.get_image().set_visible(False)
499 current_pw, current_pw_entry = password_line('Password:')
500 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
501 dialog.vbox.pack_start(current_pw, False, True, 0)
503 result = dialog.run()
504 pw = current_pw_entry.get_text()
506 if result != gtk.RESPONSE_CANCEL: return pw
508 def change_password_dialog(wallet, parent, icon):
510 show_message("No seed")
514 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'
516 msg = "Please choose a password to encrypt your wallet keys"
518 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
519 dialog.set_title("Change password")
521 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
523 dialog.set_image(image)
525 if wallet.use_encryption:
526 current_pw, current_pw_entry = password_line('Current password:')
527 dialog.vbox.pack_start(current_pw, False, True, 0)
529 password, password_entry = password_line('New password:')
530 dialog.vbox.pack_start(password, False, True, 5)
531 password2, password2_entry = password_line('Confirm password:')
532 dialog.vbox.pack_start(password2, False, True, 5)
535 result = dialog.run()
536 password = current_pw_entry.get_text() if wallet.use_encryption else None
537 new_password = password_entry.get_text()
538 new_password2 = password2_entry.get_text()
540 if result == gtk.RESPONSE_CANCEL:
544 seed = wallet.pw_decode( wallet.seed, password)
546 show_message("Incorrect password")
549 if new_password != new_password2:
550 show_message("passwords do not match")
553 wallet.update_password(seed, password, new_password)
556 if wallet.use_encryption:
557 icon.set_tooltip_text('wallet is encrypted')
559 icon.set_tooltip_text('wallet is unencrypted')
562 def add_help_button(hbox, message):
563 button = gtk.Button('?')
564 button.connect("clicked", lambda x: show_message(message))
566 hbox.pack_start(button,False, False)
569 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
571 gobject.type_register(MyWindow)
572 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
573 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
576 class ElectrumWindow:
578 def show_message(self, msg):
579 show_message(msg, self.window)
581 def __init__(self, wallet, config):
584 self.funds_error = False # True if not enough funds
586 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
587 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
588 if not self.wallet.seed: title += ' [seedless]'
589 self.window.set_title(title)
590 self.window.connect("destroy", gtk.main_quit)
591 self.window.set_border_width(0)
592 self.window.connect('mykeypress', gtk.main_quit)
593 self.window.set_default_size(720, 350)
597 self.notebook = gtk.Notebook()
598 self.create_history_tab()
600 self.create_send_tab()
601 self.create_recv_tab()
602 self.create_book_tab()
603 self.create_about_tab()
605 vbox.pack_start(self.notebook, True, True, 2)
607 self.status_bar = gtk.Statusbar()
608 vbox.pack_start(self.status_bar, False, False, 0)
610 self.status_image = gtk.Image()
611 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
612 self.status_image.set_alignment(True, 0.5 )
613 self.status_image.show()
615 self.network_button = gtk.Button()
616 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
617 self.network_button.add(self.status_image)
618 self.network_button.set_relief(gtk.RELIEF_NONE)
619 self.network_button.show()
620 self.status_bar.pack_end(self.network_button, False, False)
623 def seedb(w, wallet):
624 if wallet.use_encryption:
625 password = password_dialog(self.window)
626 if not password: return
627 else: password = None
628 show_seed_dialog(wallet, password, self.window)
629 button = gtk.Button('S')
630 button.connect("clicked", seedb, wallet )
631 button.set_relief(gtk.RELIEF_NONE)
633 self.status_bar.pack_end(button,False, False)
635 settings_icon = gtk.Image()
636 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
637 settings_icon.set_alignment(0.5, 0.5)
638 settings_icon.set_size_request(16,16 )
641 prefs_button = gtk.Button()
642 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
643 prefs_button.add(settings_icon)
644 prefs_button.set_tooltip_text("Settings")
645 prefs_button.set_relief(gtk.RELIEF_NONE)
647 self.status_bar.pack_end(prefs_button,False,False)
649 pw_icon = gtk.Image()
650 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
651 pw_icon.set_alignment(0.5, 0.5)
652 pw_icon.set_size_request(16,16 )
656 password_button = gtk.Button()
657 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
658 password_button.add(pw_icon)
659 password_button.set_relief(gtk.RELIEF_NONE)
660 password_button.show()
661 self.status_bar.pack_end(password_button,False,False)
663 self.window.add(vbox)
664 self.window.show_all()
667 self.context_id = self.status_bar.get_context_id("statusbar")
668 self.update_status_bar()
670 self.wallet_updated = False
671 self.wallet.interface.register_callback('updated', self.update_callback)
674 def update_status_bar_thread():
676 gobject.idle_add( self.update_status_bar )
680 def check_recipient_thread():
684 if self.payto_entry.is_focus():
686 r = self.payto_entry.get_text()
690 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
692 to_address = self.wallet.get_alias(r, interactive=False)
696 s = r + ' <' + to_address + '>'
697 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
700 thread.start_new_thread(update_status_bar_thread, ())
702 thread.start_new_thread(check_recipient_thread, ())
703 self.notebook.set_current_page(0)
705 def update_callback(self):
706 self.wallet_updated = True
709 def add_tab(self, page, name):
710 tab_label = gtk.Label(name)
712 self.notebook.append_page(page, tab_label)
715 def create_send_tab(self):
717 page = vbox = gtk.VBox()
721 payto_label = gtk.Label('Pay to:')
722 payto_label.set_size_request(100,-1)
723 payto.pack_start(payto_label, False)
724 payto_entry = gtk.Entry()
725 payto_entry.set_size_request(450, 26)
726 payto.pack_start(payto_entry, False)
727 vbox.pack_start(payto, False, False, 5)
730 message_label = gtk.Label('Description:')
731 message_label.set_size_request(100,-1)
732 message.pack_start(message_label, False)
733 message_entry = gtk.Entry()
734 message_entry.set_size_request(450, 26)
735 message.pack_start(message_entry, False)
736 vbox.pack_start(message, False, False, 5)
738 amount_box = gtk.HBox()
739 amount_label = gtk.Label('Amount:')
740 amount_label.set_size_request(100,-1)
741 amount_box.pack_start(amount_label, False)
742 amount_entry = gtk.Entry()
743 amount_entry.set_size_request(120, -1)
744 amount_box.pack_start(amount_entry, False)
745 vbox.pack_start(amount_box, False, False, 5)
747 self.fee_box = fee_box = gtk.HBox()
748 fee_label = gtk.Label('Fee:')
749 fee_label.set_size_request(100,-1)
750 fee_box.pack_start(fee_label, False)
751 fee_entry = gtk.Entry()
752 fee_entry.set_size_request(60, 26)
753 fee_box.pack_start(fee_entry, False)
754 vbox.pack_start(fee_box, False, False, 5)
757 empty_label = gtk.Label('')
758 empty_label.set_size_request(100,-1)
759 end_box.pack_start(empty_label, False)
760 send_button = gtk.Button("Send")
762 end_box.pack_start(send_button, False, False, 0)
763 clear_button = gtk.Button("Clear")
765 end_box.pack_start(clear_button, False, False, 15)
766 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
767 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
769 vbox.pack_start(end_box, False, False, 5)
771 # display this line only if there is a signature
772 payto_sig = gtk.HBox()
773 payto_sig_id = gtk.Label('')
774 payto_sig.pack_start(payto_sig_id, False)
775 vbox.pack_start(payto_sig, True, True, 5)
778 self.user_fee = False
780 def entry_changed( entry, is_fee ):
781 self.funds_error = False
782 amount = numbify(amount_entry)
783 fee = numbify(fee_entry)
784 if not is_fee: fee = None
787 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
789 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
792 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
793 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
794 send_button.set_sensitive(True)
796 send_button.set_sensitive(False)
797 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
798 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
799 self.funds_error = True
801 amount_entry.connect('changed', entry_changed, False)
802 fee_entry.connect('changed', entry_changed, True)
804 self.payto_entry = payto_entry
805 self.payto_fee_entry = fee_entry
806 self.payto_sig_id = payto_sig_id
807 self.payto_sig = payto_sig
808 self.amount_entry = amount_entry
809 self.message_entry = message_entry
810 self.add_tab(page, 'Send')
812 def set_frozen(self,entry,frozen):
814 entry.set_editable(False)
815 entry.set_has_frame(False)
816 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
818 entry.set_editable(True)
819 entry.set_has_frame(True)
820 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
822 def set_url(self, url):
823 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
824 self.notebook.set_current_page(1)
825 self.payto_entry.set_text(payto)
826 self.message_entry.set_text(message)
827 self.amount_entry.set_text(amount)
829 self.set_frozen(self.payto_entry,True)
830 self.set_frozen(self.amount_entry,True)
831 self.set_frozen(self.message_entry,True)
832 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
834 self.payto_sig.set_visible(False)
836 def create_about_tab(self):
841 tv.set_editable(False)
842 tv.set_cursor_visible(False)
843 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
845 self.info = tv.get_buffer()
846 self.add_tab(page, 'Wall')
848 def do_clear(self, w, data):
849 self.payto_sig.set_visible(False)
850 self.payto_fee_entry.set_text('')
851 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
852 self.set_frozen(entry,False)
855 def question(self,msg):
856 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
858 result = dialog.run()
860 return result == gtk.RESPONSE_OK
862 def do_send(self, w, data):
863 payto_entry, label_entry, amount_entry, fee_entry = data
864 label = label_entry.get_text()
865 r = payto_entry.get_text()
868 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
869 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
872 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
876 self.update_sending_tab()
879 to_address = m2.group(5)
883 if not self.wallet.is_valid(to_address):
884 self.show_message( "invalid bitcoin address:\n"+to_address)
888 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
890 self.show_message( "invalid amount")
893 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
895 self.show_message( "invalid fee")
898 if self.wallet.use_encryption:
899 password = password_dialog(self.window)
906 tx = self.wallet.mktx( to_address, amount, label, password, fee )
907 except BaseException, e:
908 self.show_message(str(e))
911 status, msg = self.wallet.sendtx( tx )
913 self.show_message( "payment sent.\n" + msg )
914 payto_entry.set_text("")
915 label_entry.set_text("")
916 amount_entry.set_text("")
917 fee_entry.set_text("")
919 self.update_sending_tab()
921 self.show_message( msg )
924 def treeview_button_press(self, treeview, event):
925 if event.type == gtk.gdk._2BUTTON_PRESS:
926 c = treeview.get_cursor()[0]
927 if treeview == self.history_treeview:
928 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
929 self.show_message(tx_details)
930 elif treeview == self.contacts_treeview:
931 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
932 a = self.wallet.aliases.get(m)
934 if a[0] in self.wallet.authorities.keys():
935 s = self.wallet.authorities.get(a[0])
938 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
939 self.show_message(msg)
942 def treeview_key_press(self, treeview, event):
943 c = treeview.get_cursor()[0]
944 if event.keyval == gtk.keysyms.Up:
946 treeview.parent.grab_focus()
947 treeview.set_cursor((0,))
948 elif event.keyval == gtk.keysyms.Return:
949 if treeview == self.history_treeview:
950 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
951 self.show_message(tx_details)
952 elif treeview == self.contacts_treeview:
953 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
954 a = self.wallet.aliases.get(m)
956 if a[0] in self.wallet.authorities.keys():
957 s = self.wallet.authorities.get(a[0])
960 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
961 self.show_message(msg)
965 def create_history_tab(self):
967 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
968 treeview = gtk.TreeView(model=self.history_list)
969 self.history_treeview = treeview
970 treeview.set_tooltip_column(7)
972 treeview.connect('key-press-event', self.treeview_key_press)
973 treeview.connect('button-press-event', self.treeview_button_press)
975 tvcolumn = gtk.TreeViewColumn('')
976 treeview.append_column(tvcolumn)
977 cell = gtk.CellRendererPixbuf()
978 tvcolumn.pack_start(cell, False)
979 tvcolumn.set_attributes(cell, stock_id=1)
981 tvcolumn = gtk.TreeViewColumn('Date')
982 treeview.append_column(tvcolumn)
983 cell = gtk.CellRendererText()
984 tvcolumn.pack_start(cell, False)
985 tvcolumn.add_attribute(cell, 'text', 2)
987 tvcolumn = gtk.TreeViewColumn('Description')
988 treeview.append_column(tvcolumn)
989 cell = gtk.CellRendererText()
990 cell.set_property('foreground', 'grey')
991 cell.set_property('family', MONOSPACE_FONT)
992 cell.set_property('editable', True)
993 def edited_cb(cell, path, new_text, h_list):
994 tx = h_list.get_value( h_list.get_iter(path), 0)
995 self.wallet.labels[tx] = new_text
997 self.update_history_tab()
998 cell.connect('edited', edited_cb, self.history_list)
999 def editing_started(cell, entry, path, h_list):
1000 tx = h_list.get_value( h_list.get_iter(path), 0)
1001 if not self.wallet.labels.get(tx): entry.set_text('')
1002 cell.connect('editing-started', editing_started, self.history_list)
1003 tvcolumn.set_expand(True)
1004 tvcolumn.pack_start(cell, True)
1005 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
1007 tvcolumn = gtk.TreeViewColumn('Amount')
1008 treeview.append_column(tvcolumn)
1009 cell = gtk.CellRendererText()
1010 cell.set_alignment(1, 0.5)
1011 cell.set_property('family', MONOSPACE_FONT)
1012 tvcolumn.pack_start(cell, False)
1013 tvcolumn.add_attribute(cell, 'text', 5)
1015 tvcolumn = gtk.TreeViewColumn('Balance')
1016 treeview.append_column(tvcolumn)
1017 cell = gtk.CellRendererText()
1018 cell.set_alignment(1, 0.5)
1019 cell.set_property('family', MONOSPACE_FONT)
1020 tvcolumn.pack_start(cell, False)
1021 tvcolumn.add_attribute(cell, 'text', 6)
1023 tvcolumn = gtk.TreeViewColumn('Tooltip')
1024 treeview.append_column(tvcolumn)
1025 cell = gtk.CellRendererText()
1026 tvcolumn.pack_start(cell, False)
1027 tvcolumn.add_attribute(cell, 'text', 7)
1028 tvcolumn.set_visible(False)
1030 scroll = gtk.ScrolledWindow()
1031 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1032 scroll.add(treeview)
1034 self.add_tab(scroll, 'History')
1035 self.update_history_tab()
1038 def create_recv_tab(self):
1039 self.recv_list = gtk.ListStore(str, str, str)
1040 self.add_tab( self.make_address_list(True), 'Receive')
1041 self.update_receiving_tab()
1043 def create_book_tab(self):
1044 self.addressbook_list = gtk.ListStore(str, str, str)
1045 self.add_tab( self.make_address_list(False), 'Contacts')
1046 self.update_sending_tab()
1048 def make_address_list(self, is_recv):
1049 liststore = self.recv_list if is_recv else self.addressbook_list
1050 treeview = gtk.TreeView(model= liststore)
1051 treeview.connect('key-press-event', self.treeview_key_press)
1052 treeview.connect('button-press-event', self.treeview_button_press)
1055 self.contacts_treeview = treeview
1057 tvcolumn = gtk.TreeViewColumn('Address')
1058 treeview.append_column(tvcolumn)
1059 cell = gtk.CellRendererText()
1060 cell.set_property('family', MONOSPACE_FONT)
1061 tvcolumn.pack_start(cell, True)
1062 tvcolumn.add_attribute(cell, 'text', 0)
1064 tvcolumn = gtk.TreeViewColumn('Label')
1065 tvcolumn.set_expand(True)
1066 treeview.append_column(tvcolumn)
1067 cell = gtk.CellRendererText()
1068 cell.set_property('editable', True)
1069 def edited_cb2(cell, path, new_text, liststore):
1070 address = liststore.get_value( liststore.get_iter(path), 0)
1071 self.wallet.labels[address] = new_text
1073 self.wallet.update_tx_labels()
1074 self.update_receiving_tab()
1075 self.update_sending_tab()
1076 self.update_history_tab()
1077 cell.connect('edited', edited_cb2, liststore)
1078 tvcolumn.pack_start(cell, True)
1079 tvcolumn.add_attribute(cell, 'text', 1)
1081 tvcolumn = gtk.TreeViewColumn('Tx')
1082 treeview.append_column(tvcolumn)
1083 cell = gtk.CellRendererText()
1084 tvcolumn.pack_start(cell, True)
1085 tvcolumn.add_attribute(cell, 'text', 2)
1087 scroll = gtk.ScrolledWindow()
1088 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1089 scroll.add(treeview)
1093 button = gtk.Button("New")
1094 button.connect("clicked", self.newaddress_dialog)
1096 hbox.pack_start(button,False)
1098 def showqrcode(w, treeview, liststore):
1099 path, col = treeview.get_cursor()
1101 address = liststore.get_value(liststore.get_iter(path), 0)
1102 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1106 size = qr.getModuleCount()*boxsize
1107 def area_expose_cb(area, event):
1108 style = area.get_style()
1109 k = qr.getModuleCount()
1112 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1113 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1114 area = gtk.DrawingArea()
1115 area.set_size_request(size, size)
1116 area.connect("expose-event", area_expose_cb)
1118 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1119 dialog.vbox.add(area)
1123 button = gtk.Button("QR")
1124 button.connect("clicked", showqrcode, treeview, liststore)
1126 hbox.pack_start(button,False)
1128 button = gtk.Button("Copy to clipboard")
1129 def copy2clipboard(w, treeview, liststore):
1131 path, col = treeview.get_cursor()
1133 address = liststore.get_value( liststore.get_iter(path), 0)
1134 if platform.system() == 'Windows':
1135 from Tkinter import Tk
1139 r.clipboard_append( address )
1142 c = gtk.clipboard_get()
1143 c.set_text( address )
1144 button.connect("clicked", copy2clipboard, treeview, liststore)
1146 hbox.pack_start(button,False)
1149 button = gtk.Button("Pay to")
1150 def payto(w, treeview, liststore):
1151 path, col = treeview.get_cursor()
1153 address = liststore.get_value( liststore.get_iter(path), 0)
1154 self.payto_entry.set_text( address )
1155 self.notebook.set_current_page(1)
1156 self.amount_entry.grab_focus()
1158 button.connect("clicked", payto, treeview, liststore)
1160 hbox.pack_start(button,False)
1163 vbox.pack_start(scroll,True)
1164 vbox.pack_start(hbox, False)
1167 def update_status_bar(self):
1168 interface = self.wallet.interface
1169 if self.funds_error:
1170 text = "Not enough funds"
1171 elif interface and interface.is_connected:
1172 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1173 if not self.wallet.up_to_date:
1174 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1175 text = "Synchronizing..."
1177 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1178 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1179 c, u = self.wallet.get_balance()
1180 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1181 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1183 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1184 self.network_button.set_tooltip_text("Not connected.")
1185 text = "Not connected"
1187 self.status_bar.pop(self.context_id)
1188 self.status_bar.push(self.context_id, text)
1190 if self.wallet.up_to_date and self.wallet_updated:
1191 self.update_history_tab()
1192 self.update_receiving_tab()
1193 # addressbook too...
1194 self.info.set_text( self.wallet.banner )
1195 self.wallet_updated = False
1197 def update_receiving_tab(self):
1198 self.recv_list.clear()
1199 for address in self.wallet.all_addresses():
1200 if self.wallet.is_change(address):continue
1201 label = self.wallet.labels.get(address)
1202 h = self.wallet.history.get(address,[])
1204 tx = "None" if n==0 else "%d"%n
1205 self.recv_list.append((address, label, tx ))
1207 def update_sending_tab(self):
1208 # detect addresses that are not mine in history, add them here...
1209 self.addressbook_list.clear()
1210 for alias, v in self.wallet.aliases.items():
1212 label = self.wallet.labels.get(alias)
1213 self.addressbook_list.append((alias, label, '-'))
1215 for address in self.wallet.addressbook:
1216 label = self.wallet.labels.get(address)
1218 for item in self.wallet.transactions.values():
1219 if address in item['outputs'] : n=n+1
1220 tx = "None" if n==0 else "%d"%n
1221 self.addressbook_list.append((address, label, tx))
1223 def update_history_tab(self):
1224 cursor = self.history_treeview.get_cursor()[0]
1225 self.history_list.clear()
1227 for tx in self.wallet.get_tx_history():
1228 tx_hash = tx['tx_hash']
1229 conf = self.wallet.verifier.get_confirmations(tx_hash)
1231 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
1232 conf_icon = gtk.STOCK_APPLY
1234 time_str = 'pending'
1235 conf_icon = gtk.STOCK_EXECUTE
1236 v = self.wallet.get_tx_value(tx_hash)
1238 label, is_default_label = self.wallet.get_label(tx_hash)
1239 tooltip = tx_hash + "\n%d confirmations"%conf
1241 inputs = map(lambda x: x.get('address'), tx['inputs'])
1242 outputs = map(lambda x: x.get('address'), tx['outputs'])
1243 details = "Transaction Details:\n\n" \
1244 + "Transaction ID:\n" + tx_hash + "\n\n" \
1245 + "Status: %d confirmations\n\n"%conf \
1246 + "Date: %s\n\n"%time_str \
1247 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1248 + "Outputs:\n-"+ '\n-'.join(outputs)
1249 r = self.wallet.receipts.get(tx_hash)
1251 details += "\n_______________________________________" \
1252 + '\n\nSigned URI: ' + r[2] \
1253 + "\n\nSigned by: " + r[0] \
1254 + '\n\nSignature: ' + r[1]
1257 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1258 format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1259 if cursor: self.history_treeview.set_cursor( cursor )
1263 def newaddress_dialog(self, w):
1265 title = "New Contact"
1266 dialog = gtk.Dialog(title, parent=self.window,
1267 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1268 buttons= ("cancel", 0, "ok",1) )
1272 label_label = gtk.Label('Label:')
1273 label_label.set_size_request(120,10)
1275 label.pack_start(label_label)
1276 label_entry = gtk.Entry()
1278 label.pack_start(label_entry)
1280 dialog.vbox.pack_start(label, False, True, 5)
1282 address = gtk.HBox()
1283 address_label = gtk.Label('Address:')
1284 address_label.set_size_request(120,10)
1285 address_label.show()
1286 address.pack_start(address_label)
1287 address_entry = gtk.Entry()
1288 address_entry.show()
1289 address.pack_start(address_entry)
1291 dialog.vbox.pack_start(address, False, True, 5)
1293 result = dialog.run()
1294 address = address_entry.get_text()
1295 label = label_entry.get_text()
1299 if self.wallet.is_valid(address):
1300 self.wallet.addressbook.append(address)
1301 if label: self.wallet.labels[address] = label
1303 self.update_sending_tab()
1305 errorDialog = gtk.MessageDialog(
1307 flags=gtk.DIALOG_MODAL,
1308 buttons= gtk.BUTTONS_CLOSE,
1309 message_format = "Invalid address")
1312 errorDialog.destroy()
1316 class ElectrumGui():
1318 def __init__(self, wallet, config):
1319 self.wallet = wallet
1320 self.config = config
1322 def main(self, url=None):
1323 ew = ElectrumWindow(self.wallet, self.config)
1324 if url: ew.set_url(url)
1327 def restore_or_create(self):
1328 return restore_create_dialog(self.wallet)
1330 def server_list_changed(self):