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_combo.append_text('Lite')
277 gui_combo.append_text('Classic')
278 gui_combo.append_text('Gtk')
280 gui_box.pack_start(gui_combo,False, False, 10)
281 gui_names = ['lite','classic','gtk']
282 gui_combo.set_active( gui_names.index( wallet.config.get("gui","lite")) )
284 add_help_button(gui_box, "Select which GUI mode to use at start up.")
286 vbox.pack_start(gui_box, False,False, 5)
290 fee = fee_entry.get_text()
291 nz = nz_entry.get_text()
292 gui = gui_names[ gui_combo.get_active()]
295 if r==gtk.RESPONSE_CANCEL:
299 fee = int( 100000000 * Decimal(fee) )
301 show_message("error")
303 if wallet.fee != fee:
311 show_message("error")
313 if wallet.num_zeros != nz:
314 wallet.num_zeros = nz
317 wallet.config.set_key('gui',gui,True)
322 def run_network_dialog( wallet, parent ):
324 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
325 interface = wallet.interface
327 if interface.is_connected:
328 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
330 status = "Not connected"
333 status = "Please choose a server."
335 server = interface.server
337 if not wallet.interface.servers:
339 for x in DEFAULT_SERVERS:
340 h,port,protocol = x.split(':')
341 servers_list.append( (h,[(protocol,port)] ) )
343 servers_list = wallet.interface.servers
346 for item in servers_list:
350 protocol, port = item2
354 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
355 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
356 dialog.set_title("Server")
357 dialog.set_image(image)
361 host_box = gtk.HBox()
362 host_label = gtk.Label('Connect to:')
363 host_label.set_size_request(100,-1)
365 host_box.pack_start(host_label, False, False, 10)
366 host_entry = gtk.Entry()
367 host_entry.set_size_request(200,-1)
368 host_entry.set_text(server)
370 host_box.pack_start(host_entry, False, False, 10)
371 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)')
375 p_box = gtk.HBox(False, 10)
378 p_label = gtk.Label('Protocol:')
379 p_label.set_size_request(100,-1)
381 p_box.pack_start(p_label, False, False, 10)
383 radio1 = gtk.RadioButton(None, "tcp")
384 p_box.pack_start(radio1, True, True, 0)
386 radio2 = gtk.RadioButton(radio1, "http")
387 p_box.pack_start(radio2, True, True, 0)
391 return unicode(host_entry.get_text()).split(':')
393 def set_button(protocol):
396 elif protocol == 'h':
399 def set_protocol(protocol):
400 host = current_line()[0]
402 if protocol not in pp.keys():
403 protocol = pp.keys()[0]
406 host_entry.set_text( host + ':' + port + ':' + protocol)
408 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
409 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
411 server_list = gtk.ListStore(str)
412 for host in plist.keys():
413 server_list.append([host])
415 treeview = gtk.TreeView(model=server_list)
418 if wallet.interface.servers:
419 label = 'Active Servers'
421 label = 'Default Servers'
423 tvcolumn = gtk.TreeViewColumn(label)
424 treeview.append_column(tvcolumn)
425 cell = gtk.CellRendererText()
426 tvcolumn.pack_start(cell, False)
427 tvcolumn.add_attribute(cell, 'text', 0)
429 scroll = gtk.ScrolledWindow()
430 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
434 vbox.pack_start(host_box, False,False, 5)
435 vbox.pack_start(p_box, True, True, 0)
436 vbox.pack_start(scroll)
438 def my_treeview_cb(treeview):
439 path, view_column = treeview.get_cursor()
440 host = server_list.get_value( server_list.get_iter(path), 0)
446 protocol = pp.keys()[0]
448 host_entry.set_text( host + ':' + port + ':' + protocol)
451 treeview.connect('cursor-changed', my_treeview_cb)
455 server = host_entry.get_text()
458 if r==gtk.RESPONSE_CANCEL:
462 interface.set_server(server)
464 show_message("error:" + server)
468 wallet.config.set_key("server", server, True)
473 def show_message(message, parent=None):
474 dialog = gtk.MessageDialog(
476 flags = gtk.DIALOG_MODAL,
477 buttons = gtk.BUTTONS_CLOSE,
478 message_format = message )
483 def password_line(label):
484 password = gtk.HBox()
485 password_label = gtk.Label(label)
486 password_label.set_size_request(120,10)
487 password_label.show()
488 password.pack_start(password_label,False, False, 10)
489 password_entry = gtk.Entry()
490 password_entry.set_size_request(300,-1)
491 password_entry.set_visibility(False)
492 password_entry.show()
493 password.pack_start(password_entry,False,False, 10)
495 return password, password_entry
497 def password_dialog(parent):
498 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
499 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
500 dialog.get_image().set_visible(False)
501 current_pw, current_pw_entry = password_line('Password:')
502 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
503 dialog.vbox.pack_start(current_pw, False, True, 0)
505 result = dialog.run()
506 pw = current_pw_entry.get_text()
508 if result != gtk.RESPONSE_CANCEL: return pw
510 def change_password_dialog(wallet, parent, icon):
512 show_message("No seed")
516 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'
518 msg = "Please choose a password to encrypt your wallet keys"
520 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
521 dialog.set_title("Change password")
523 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
525 dialog.set_image(image)
527 if wallet.use_encryption:
528 current_pw, current_pw_entry = password_line('Current password:')
529 dialog.vbox.pack_start(current_pw, False, True, 0)
531 password, password_entry = password_line('New password:')
532 dialog.vbox.pack_start(password, False, True, 5)
533 password2, password2_entry = password_line('Confirm password:')
534 dialog.vbox.pack_start(password2, False, True, 5)
537 result = dialog.run()
538 password = current_pw_entry.get_text() if wallet.use_encryption else None
539 new_password = password_entry.get_text()
540 new_password2 = password2_entry.get_text()
542 if result == gtk.RESPONSE_CANCEL:
546 seed = wallet.pw_decode( wallet.seed, password)
548 show_message("Incorrect password")
551 if new_password != new_password2:
552 show_message("passwords do not match")
555 wallet.update_password(seed, password, new_password)
558 if wallet.use_encryption:
559 icon.set_tooltip_text('wallet is encrypted')
561 icon.set_tooltip_text('wallet is unencrypted')
564 def add_help_button(hbox, message):
565 button = gtk.Button('?')
566 button.connect("clicked", lambda x: show_message(message))
568 hbox.pack_start(button,False, False)
571 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
573 gobject.type_register(MyWindow)
574 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
575 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
578 class ElectrumWindow:
580 def show_message(self, msg):
581 show_message(msg, self.window)
583 def __init__(self, wallet, config):
586 self.funds_error = False # True if not enough funds
588 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
589 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
590 if not self.wallet.seed: title += ' [seedless]'
591 self.window.set_title(title)
592 self.window.connect("destroy", gtk.main_quit)
593 self.window.set_border_width(0)
594 self.window.connect('mykeypress', gtk.main_quit)
595 self.window.set_default_size(720, 350)
599 self.notebook = gtk.Notebook()
600 self.create_history_tab()
602 self.create_send_tab()
603 self.create_recv_tab()
604 self.create_book_tab()
605 self.create_about_tab()
607 vbox.pack_start(self.notebook, True, True, 2)
609 self.status_bar = gtk.Statusbar()
610 vbox.pack_start(self.status_bar, False, False, 0)
612 self.status_image = gtk.Image()
613 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
614 self.status_image.set_alignment(True, 0.5 )
615 self.status_image.show()
617 self.network_button = gtk.Button()
618 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
619 self.network_button.add(self.status_image)
620 self.network_button.set_relief(gtk.RELIEF_NONE)
621 self.network_button.show()
622 self.status_bar.pack_end(self.network_button, False, False)
625 def seedb(w, wallet):
626 if wallet.use_encryption:
627 password = password_dialog(self.window)
628 if not password: return
629 else: password = None
630 show_seed_dialog(wallet, password, self.window)
631 button = gtk.Button('S')
632 button.connect("clicked", seedb, wallet )
633 button.set_relief(gtk.RELIEF_NONE)
635 self.status_bar.pack_end(button,False, False)
637 settings_icon = gtk.Image()
638 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
639 settings_icon.set_alignment(0.5, 0.5)
640 settings_icon.set_size_request(16,16 )
643 prefs_button = gtk.Button()
644 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
645 prefs_button.add(settings_icon)
646 prefs_button.set_tooltip_text("Settings")
647 prefs_button.set_relief(gtk.RELIEF_NONE)
649 self.status_bar.pack_end(prefs_button,False,False)
651 pw_icon = gtk.Image()
652 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
653 pw_icon.set_alignment(0.5, 0.5)
654 pw_icon.set_size_request(16,16 )
658 password_button = gtk.Button()
659 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
660 password_button.add(pw_icon)
661 password_button.set_relief(gtk.RELIEF_NONE)
662 password_button.show()
663 self.status_bar.pack_end(password_button,False,False)
665 self.window.add(vbox)
666 self.window.show_all()
669 self.context_id = self.status_bar.get_context_id("statusbar")
670 self.update_status_bar()
672 def update_status_bar_thread():
674 gobject.idle_add( self.update_status_bar )
678 def check_recipient_thread():
682 if self.payto_entry.is_focus():
684 r = self.payto_entry.get_text()
688 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
690 to_address = self.wallet.get_alias(r, interactive=False)
694 s = r + ' <' + to_address + '>'
695 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
698 thread.start_new_thread(update_status_bar_thread, ())
700 thread.start_new_thread(check_recipient_thread, ())
701 self.notebook.set_current_page(0)
704 def add_tab(self, page, name):
705 tab_label = gtk.Label(name)
707 self.notebook.append_page(page, tab_label)
710 def create_send_tab(self):
712 page = vbox = gtk.VBox()
716 payto_label = gtk.Label('Pay to:')
717 payto_label.set_size_request(100,-1)
718 payto.pack_start(payto_label, False)
719 payto_entry = gtk.Entry()
720 payto_entry.set_size_request(450, 26)
721 payto.pack_start(payto_entry, False)
722 vbox.pack_start(payto, False, False, 5)
725 message_label = gtk.Label('Description:')
726 message_label.set_size_request(100,-1)
727 message.pack_start(message_label, False)
728 message_entry = gtk.Entry()
729 message_entry.set_size_request(450, 26)
730 message.pack_start(message_entry, False)
731 vbox.pack_start(message, False, False, 5)
733 amount_box = gtk.HBox()
734 amount_label = gtk.Label('Amount:')
735 amount_label.set_size_request(100,-1)
736 amount_box.pack_start(amount_label, False)
737 amount_entry = gtk.Entry()
738 amount_entry.set_size_request(120, -1)
739 amount_box.pack_start(amount_entry, False)
740 vbox.pack_start(amount_box, False, False, 5)
742 self.fee_box = fee_box = gtk.HBox()
743 fee_label = gtk.Label('Fee:')
744 fee_label.set_size_request(100,-1)
745 fee_box.pack_start(fee_label, False)
746 fee_entry = gtk.Entry()
747 fee_entry.set_size_request(60, 26)
748 fee_box.pack_start(fee_entry, False)
749 vbox.pack_start(fee_box, False, False, 5)
752 empty_label = gtk.Label('')
753 empty_label.set_size_request(100,-1)
754 end_box.pack_start(empty_label, False)
755 send_button = gtk.Button("Send")
757 end_box.pack_start(send_button, False, False, 0)
758 clear_button = gtk.Button("Clear")
760 end_box.pack_start(clear_button, False, False, 15)
761 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
762 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
764 vbox.pack_start(end_box, False, False, 5)
766 # display this line only if there is a signature
767 payto_sig = gtk.HBox()
768 payto_sig_id = gtk.Label('')
769 payto_sig.pack_start(payto_sig_id, False)
770 vbox.pack_start(payto_sig, True, True, 5)
773 self.user_fee = False
775 def entry_changed( entry, is_fee ):
776 self.funds_error = False
777 amount = numbify(amount_entry)
778 fee = numbify(fee_entry)
779 if not is_fee: fee = None
782 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
784 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
787 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
788 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
789 send_button.set_sensitive(True)
791 send_button.set_sensitive(False)
792 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
793 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
794 self.funds_error = True
796 amount_entry.connect('changed', entry_changed, False)
797 fee_entry.connect('changed', entry_changed, True)
799 self.payto_entry = payto_entry
800 self.payto_fee_entry = fee_entry
801 self.payto_sig_id = payto_sig_id
802 self.payto_sig = payto_sig
803 self.amount_entry = amount_entry
804 self.message_entry = message_entry
805 self.add_tab(page, 'Send')
807 def set_frozen(self,entry,frozen):
809 entry.set_editable(False)
810 entry.set_has_frame(False)
811 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
813 entry.set_editable(True)
814 entry.set_has_frame(True)
815 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
817 def set_url(self, url):
818 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
819 self.notebook.set_current_page(1)
820 self.payto_entry.set_text(payto)
821 self.message_entry.set_text(message)
822 self.amount_entry.set_text(amount)
824 self.set_frozen(self.payto_entry,True)
825 self.set_frozen(self.amount_entry,True)
826 self.set_frozen(self.message_entry,True)
827 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
829 self.payto_sig.set_visible(False)
831 def create_about_tab(self):
836 tv.set_editable(False)
837 tv.set_cursor_visible(False)
838 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
840 self.info = tv.get_buffer()
841 self.add_tab(page, 'Wall')
843 def do_clear(self, w, data):
844 self.payto_sig.set_visible(False)
845 self.payto_fee_entry.set_text('')
846 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
847 self.set_frozen(entry,False)
850 def question(self,msg):
851 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
853 result = dialog.run()
855 return result == gtk.RESPONSE_OK
857 def do_send(self, w, data):
858 payto_entry, label_entry, amount_entry, fee_entry = data
859 label = label_entry.get_text()
860 r = payto_entry.get_text()
863 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
864 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
867 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
871 self.update_sending_tab()
874 to_address = m2.group(5)
878 if not self.wallet.is_valid(to_address):
879 self.show_message( "invalid bitcoin address:\n"+to_address)
883 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
885 self.show_message( "invalid amount")
888 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
890 self.show_message( "invalid fee")
893 if self.wallet.use_encryption:
894 password = password_dialog(self.window)
901 tx = self.wallet.mktx( to_address, amount, label, password, fee )
902 except BaseException, e:
903 self.show_message(str(e))
906 status, msg = self.wallet.sendtx( tx )
908 self.show_message( "payment sent.\n" + msg )
909 payto_entry.set_text("")
910 label_entry.set_text("")
911 amount_entry.set_text("")
912 fee_entry.set_text("")
914 self.update_sending_tab()
916 self.show_message( msg )
919 def treeview_button_press(self, treeview, event):
920 if event.type == gtk.gdk._2BUTTON_PRESS:
921 c = treeview.get_cursor()[0]
922 if treeview == self.history_treeview:
923 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
924 self.show_message(tx_details)
925 elif treeview == self.contacts_treeview:
926 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
927 a = self.wallet.aliases.get(m)
929 if a[0] in self.wallet.authorities.keys():
930 s = self.wallet.authorities.get(a[0])
933 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
934 self.show_message(msg)
937 def treeview_key_press(self, treeview, event):
938 c = treeview.get_cursor()[0]
939 if event.keyval == gtk.keysyms.Up:
941 treeview.parent.grab_focus()
942 treeview.set_cursor((0,))
943 elif event.keyval == gtk.keysyms.Return:
944 if treeview == self.history_treeview:
945 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
946 self.show_message(tx_details)
947 elif treeview == self.contacts_treeview:
948 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
949 a = self.wallet.aliases.get(m)
951 if a[0] in self.wallet.authorities.keys():
952 s = self.wallet.authorities.get(a[0])
955 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
956 self.show_message(msg)
960 def create_history_tab(self):
962 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
963 treeview = gtk.TreeView(model=self.history_list)
964 self.history_treeview = treeview
965 treeview.set_tooltip_column(7)
967 treeview.connect('key-press-event', self.treeview_key_press)
968 treeview.connect('button-press-event', self.treeview_button_press)
970 tvcolumn = gtk.TreeViewColumn('')
971 treeview.append_column(tvcolumn)
972 cell = gtk.CellRendererPixbuf()
973 tvcolumn.pack_start(cell, False)
974 tvcolumn.set_attributes(cell, stock_id=1)
976 tvcolumn = gtk.TreeViewColumn('Date')
977 treeview.append_column(tvcolumn)
978 cell = gtk.CellRendererText()
979 tvcolumn.pack_start(cell, False)
980 tvcolumn.add_attribute(cell, 'text', 2)
982 tvcolumn = gtk.TreeViewColumn('Description')
983 treeview.append_column(tvcolumn)
984 cell = gtk.CellRendererText()
985 cell.set_property('foreground', 'grey')
986 cell.set_property('family', MONOSPACE_FONT)
987 cell.set_property('editable', True)
988 def edited_cb(cell, path, new_text, h_list):
989 tx = h_list.get_value( h_list.get_iter(path), 0)
990 self.wallet.labels[tx] = new_text
992 self.update_history_tab()
993 cell.connect('edited', edited_cb, self.history_list)
994 def editing_started(cell, entry, path, h_list):
995 tx = h_list.get_value( h_list.get_iter(path), 0)
996 if not self.wallet.labels.get(tx): entry.set_text('')
997 cell.connect('editing-started', editing_started, self.history_list)
998 tvcolumn.set_expand(True)
999 tvcolumn.pack_start(cell, True)
1000 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
1002 tvcolumn = gtk.TreeViewColumn('Amount')
1003 treeview.append_column(tvcolumn)
1004 cell = gtk.CellRendererText()
1005 cell.set_alignment(1, 0.5)
1006 cell.set_property('family', MONOSPACE_FONT)
1007 tvcolumn.pack_start(cell, False)
1008 tvcolumn.add_attribute(cell, 'text', 5)
1010 tvcolumn = gtk.TreeViewColumn('Balance')
1011 treeview.append_column(tvcolumn)
1012 cell = gtk.CellRendererText()
1013 cell.set_alignment(1, 0.5)
1014 cell.set_property('family', MONOSPACE_FONT)
1015 tvcolumn.pack_start(cell, False)
1016 tvcolumn.add_attribute(cell, 'text', 6)
1018 tvcolumn = gtk.TreeViewColumn('Tooltip')
1019 treeview.append_column(tvcolumn)
1020 cell = gtk.CellRendererText()
1021 tvcolumn.pack_start(cell, False)
1022 tvcolumn.add_attribute(cell, 'text', 7)
1023 tvcolumn.set_visible(False)
1025 scroll = gtk.ScrolledWindow()
1026 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1027 scroll.add(treeview)
1029 self.add_tab(scroll, 'History')
1030 self.update_history_tab()
1033 def create_recv_tab(self):
1034 self.recv_list = gtk.ListStore(str, str, str)
1035 self.add_tab( self.make_address_list(True), 'Receive')
1036 self.update_receiving_tab()
1038 def create_book_tab(self):
1039 self.addressbook_list = gtk.ListStore(str, str, str)
1040 self.add_tab( self.make_address_list(False), 'Contacts')
1041 self.update_sending_tab()
1043 def make_address_list(self, is_recv):
1044 liststore = self.recv_list if is_recv else self.addressbook_list
1045 treeview = gtk.TreeView(model= liststore)
1046 treeview.connect('key-press-event', self.treeview_key_press)
1047 treeview.connect('button-press-event', self.treeview_button_press)
1050 self.contacts_treeview = treeview
1052 tvcolumn = gtk.TreeViewColumn('Address')
1053 treeview.append_column(tvcolumn)
1054 cell = gtk.CellRendererText()
1055 cell.set_property('family', MONOSPACE_FONT)
1056 tvcolumn.pack_start(cell, True)
1057 tvcolumn.add_attribute(cell, 'text', 0)
1059 tvcolumn = gtk.TreeViewColumn('Label')
1060 tvcolumn.set_expand(True)
1061 treeview.append_column(tvcolumn)
1062 cell = gtk.CellRendererText()
1063 cell.set_property('editable', True)
1064 def edited_cb2(cell, path, new_text, liststore):
1065 address = liststore.get_value( liststore.get_iter(path), 0)
1066 self.wallet.labels[address] = new_text
1068 self.wallet.update_tx_labels()
1069 self.update_receiving_tab()
1070 self.update_sending_tab()
1071 self.update_history_tab()
1072 cell.connect('edited', edited_cb2, liststore)
1073 tvcolumn.pack_start(cell, True)
1074 tvcolumn.add_attribute(cell, 'text', 1)
1076 tvcolumn = gtk.TreeViewColumn('Tx')
1077 treeview.append_column(tvcolumn)
1078 cell = gtk.CellRendererText()
1079 tvcolumn.pack_start(cell, True)
1080 tvcolumn.add_attribute(cell, 'text', 2)
1082 scroll = gtk.ScrolledWindow()
1083 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1084 scroll.add(treeview)
1088 button = gtk.Button("New")
1089 button.connect("clicked", self.newaddress_dialog)
1091 hbox.pack_start(button,False)
1093 def showqrcode(w, treeview, liststore):
1094 path, col = treeview.get_cursor()
1096 address = liststore.get_value(liststore.get_iter(path), 0)
1097 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1101 size = qr.getModuleCount()*boxsize
1102 def area_expose_cb(area, event):
1103 style = area.get_style()
1104 k = qr.getModuleCount()
1107 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1108 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1109 area = gtk.DrawingArea()
1110 area.set_size_request(size, size)
1111 area.connect("expose-event", area_expose_cb)
1113 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1114 dialog.vbox.add(area)
1118 button = gtk.Button("QR")
1119 button.connect("clicked", showqrcode, treeview, liststore)
1121 hbox.pack_start(button,False)
1123 button = gtk.Button("Copy to clipboard")
1124 def copy2clipboard(w, treeview, liststore):
1126 path, col = treeview.get_cursor()
1128 address = liststore.get_value( liststore.get_iter(path), 0)
1129 if platform.system() == 'Windows':
1130 from Tkinter import Tk
1134 r.clipboard_append( address )
1137 c = gtk.clipboard_get()
1138 c.set_text( address )
1139 button.connect("clicked", copy2clipboard, treeview, liststore)
1141 hbox.pack_start(button,False)
1144 button = gtk.Button("Pay to")
1145 def payto(w, treeview, liststore):
1146 path, col = treeview.get_cursor()
1148 address = liststore.get_value( liststore.get_iter(path), 0)
1149 self.payto_entry.set_text( address )
1150 self.notebook.set_current_page(1)
1151 self.amount_entry.grab_focus()
1153 button.connect("clicked", payto, treeview, liststore)
1155 hbox.pack_start(button,False)
1158 vbox.pack_start(scroll,True)
1159 vbox.pack_start(hbox, False)
1162 def update_status_bar(self):
1163 interface = self.wallet.interface
1164 if self.funds_error:
1165 text = "Not enough funds"
1166 elif interface and interface.is_connected:
1167 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1168 if self.wallet.blocks == -1:
1169 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1170 text = "Connecting..."
1171 elif self.wallet.blocks == 0:
1172 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1173 text = "Server not ready"
1174 elif not self.wallet.up_to_date:
1175 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1176 text = "Synchronizing..."
1178 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1179 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1180 c, u = self.wallet.get_balance()
1181 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1182 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1184 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1185 self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(interface.server, self.wallet.blocks))
1186 text = "Not connected"
1188 self.status_bar.pop(self.context_id)
1189 self.status_bar.push(self.context_id, text)
1191 if self.wallet.was_updated and self.wallet.up_to_date:
1192 self.update_history_tab()
1193 self.update_receiving_tab()
1194 # addressbook too...
1195 self.info.set_text( self.wallet.banner )
1196 self.wallet.was_updated = False
1199 def update_receiving_tab(self):
1200 self.recv_list.clear()
1201 for address in self.wallet.all_addresses():
1202 if self.wallet.is_change(address):continue
1203 label = self.wallet.labels.get(address)
1205 h = self.wallet.history.get(address,[])
1207 if not item['is_input'] : n=n+1
1208 tx = "None" if n==0 else "%d"%n
1209 self.recv_list.append((address, label, tx ))
1211 def update_sending_tab(self):
1212 # detect addresses that are not mine in history, add them here...
1213 self.addressbook_list.clear()
1214 for alias, v in self.wallet.aliases.items():
1216 label = self.wallet.labels.get(alias)
1217 self.addressbook_list.append((alias, label, '-'))
1219 for address in self.wallet.addressbook:
1220 label = self.wallet.labels.get(address)
1222 for item in self.wallet.tx_history.values():
1223 if address in item['outputs'] : n=n+1
1224 tx = "None" if n==0 else "%d"%n
1225 self.addressbook_list.append((address, label, tx))
1227 def update_history_tab(self):
1228 cursor = self.history_treeview.get_cursor()[0]
1229 self.history_list.clear()
1231 for tx in self.wallet.get_tx_history():
1232 tx_hash = tx['tx_hash']
1234 conf = self.wallet.blocks - tx['height'] + 1
1235 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
1236 conf_icon = gtk.STOCK_APPLY
1239 time_str = 'pending'
1240 conf_icon = gtk.STOCK_EXECUTE
1243 label = self.wallet.labels.get(tx_hash)
1244 is_default_label = (label == '') or (label is None)
1245 if is_default_label: label = tx['default_label']
1246 tooltip = tx_hash + "\n%d confirmations"%conf
1248 # tx = self.wallet.tx_history.get(tx_hash)
1249 details = "Transaction Details:\n\n" \
1250 + "Transaction ID:\n" + tx_hash + "\n\n" \
1251 + "Status: %d confirmations\n\n"%conf \
1252 + "Date: %s\n\n"%time_str \
1253 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
1254 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
1255 r = self.wallet.receipts.get(tx_hash)
1257 details += "\n_______________________________________" \
1258 + '\n\nSigned URI: ' + r[2] \
1259 + "\n\nSigned by: " + r[0] \
1260 + '\n\nSignature: ' + r[1]
1263 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1264 format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1265 if cursor: self.history_treeview.set_cursor( cursor )
1269 def newaddress_dialog(self, w):
1271 title = "New Contact"
1272 dialog = gtk.Dialog(title, parent=self.window,
1273 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1274 buttons= ("cancel", 0, "ok",1) )
1278 label_label = gtk.Label('Label:')
1279 label_label.set_size_request(120,10)
1281 label.pack_start(label_label)
1282 label_entry = gtk.Entry()
1284 label.pack_start(label_entry)
1286 dialog.vbox.pack_start(label, False, True, 5)
1288 address = gtk.HBox()
1289 address_label = gtk.Label('Address:')
1290 address_label.set_size_request(120,10)
1291 address_label.show()
1292 address.pack_start(address_label)
1293 address_entry = gtk.Entry()
1294 address_entry.show()
1295 address.pack_start(address_entry)
1297 dialog.vbox.pack_start(address, False, True, 5)
1299 result = dialog.run()
1300 address = address_entry.get_text()
1301 label = label_entry.get_text()
1305 if self.wallet.is_valid(address):
1306 self.wallet.addressbook.append(address)
1307 if label: self.wallet.labels[address] = label
1309 self.update_sending_tab()
1311 errorDialog = gtk.MessageDialog(
1313 flags=gtk.DIALOG_MODAL,
1314 buttons= gtk.BUTTONS_CLOSE,
1315 message_format = "Invalid address")
1318 errorDialog.destroy()
1322 class ElectrumGui():
1324 def __init__(self, wallet, config):
1325 self.wallet = wallet
1326 self.config = config
1328 def main(self, url=None):
1329 ew = ElectrumWindow(self.wallet, self.config)
1330 if url: ew.set_url(url)
1333 def restore_or_create(self):
1334 return restore_create_dialog(self.wallet)
1336 def server_list_changed(self):