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.verifier.height)
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 self.wallet_updated = False
673 self.wallet.interface.register_callback('updated', self.update_callback)
676 def update_status_bar_thread():
678 gobject.idle_add( self.update_status_bar )
682 def check_recipient_thread():
686 if self.payto_entry.is_focus():
688 r = self.payto_entry.get_text()
692 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
694 to_address = self.wallet.get_alias(r, interactive=False)
698 s = r + ' <' + to_address + '>'
699 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
702 thread.start_new_thread(update_status_bar_thread, ())
704 thread.start_new_thread(check_recipient_thread, ())
705 self.notebook.set_current_page(0)
707 def update_callback(self):
708 self.wallet_updated = True
711 def add_tab(self, page, name):
712 tab_label = gtk.Label(name)
714 self.notebook.append_page(page, tab_label)
717 def create_send_tab(self):
719 page = vbox = gtk.VBox()
723 payto_label = gtk.Label('Pay to:')
724 payto_label.set_size_request(100,-1)
725 payto.pack_start(payto_label, False)
726 payto_entry = gtk.Entry()
727 payto_entry.set_size_request(450, 26)
728 payto.pack_start(payto_entry, False)
729 vbox.pack_start(payto, False, False, 5)
732 message_label = gtk.Label('Description:')
733 message_label.set_size_request(100,-1)
734 message.pack_start(message_label, False)
735 message_entry = gtk.Entry()
736 message_entry.set_size_request(450, 26)
737 message.pack_start(message_entry, False)
738 vbox.pack_start(message, False, False, 5)
740 amount_box = gtk.HBox()
741 amount_label = gtk.Label('Amount:')
742 amount_label.set_size_request(100,-1)
743 amount_box.pack_start(amount_label, False)
744 amount_entry = gtk.Entry()
745 amount_entry.set_size_request(120, -1)
746 amount_box.pack_start(amount_entry, False)
747 vbox.pack_start(amount_box, False, False, 5)
749 self.fee_box = fee_box = gtk.HBox()
750 fee_label = gtk.Label('Fee:')
751 fee_label.set_size_request(100,-1)
752 fee_box.pack_start(fee_label, False)
753 fee_entry = gtk.Entry()
754 fee_entry.set_size_request(60, 26)
755 fee_box.pack_start(fee_entry, False)
756 vbox.pack_start(fee_box, False, False, 5)
759 empty_label = gtk.Label('')
760 empty_label.set_size_request(100,-1)
761 end_box.pack_start(empty_label, False)
762 send_button = gtk.Button("Send")
764 end_box.pack_start(send_button, False, False, 0)
765 clear_button = gtk.Button("Clear")
767 end_box.pack_start(clear_button, False, False, 15)
768 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
769 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
771 vbox.pack_start(end_box, False, False, 5)
773 # display this line only if there is a signature
774 payto_sig = gtk.HBox()
775 payto_sig_id = gtk.Label('')
776 payto_sig.pack_start(payto_sig_id, False)
777 vbox.pack_start(payto_sig, True, True, 5)
780 self.user_fee = False
782 def entry_changed( entry, is_fee ):
783 self.funds_error = False
784 amount = numbify(amount_entry)
785 fee = numbify(fee_entry)
786 if not is_fee: fee = None
789 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
791 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
794 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
795 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
796 send_button.set_sensitive(True)
798 send_button.set_sensitive(False)
799 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
800 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
801 self.funds_error = True
803 amount_entry.connect('changed', entry_changed, False)
804 fee_entry.connect('changed', entry_changed, True)
806 self.payto_entry = payto_entry
807 self.payto_fee_entry = fee_entry
808 self.payto_sig_id = payto_sig_id
809 self.payto_sig = payto_sig
810 self.amount_entry = amount_entry
811 self.message_entry = message_entry
812 self.add_tab(page, 'Send')
814 def set_frozen(self,entry,frozen):
816 entry.set_editable(False)
817 entry.set_has_frame(False)
818 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
820 entry.set_editable(True)
821 entry.set_has_frame(True)
822 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
824 def set_url(self, url):
825 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
826 self.notebook.set_current_page(1)
827 self.payto_entry.set_text(payto)
828 self.message_entry.set_text(message)
829 self.amount_entry.set_text(amount)
831 self.set_frozen(self.payto_entry,True)
832 self.set_frozen(self.amount_entry,True)
833 self.set_frozen(self.message_entry,True)
834 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
836 self.payto_sig.set_visible(False)
838 def create_about_tab(self):
843 tv.set_editable(False)
844 tv.set_cursor_visible(False)
845 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
847 self.info = tv.get_buffer()
848 self.add_tab(page, 'Wall')
850 def do_clear(self, w, data):
851 self.payto_sig.set_visible(False)
852 self.payto_fee_entry.set_text('')
853 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
854 self.set_frozen(entry,False)
857 def question(self,msg):
858 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
860 result = dialog.run()
862 return result == gtk.RESPONSE_OK
864 def do_send(self, w, data):
865 payto_entry, label_entry, amount_entry, fee_entry = data
866 label = label_entry.get_text()
867 r = payto_entry.get_text()
870 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
871 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
874 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
878 self.update_sending_tab()
881 to_address = m2.group(5)
885 if not self.wallet.is_valid(to_address):
886 self.show_message( "invalid bitcoin address:\n"+to_address)
890 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
892 self.show_message( "invalid amount")
895 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
897 self.show_message( "invalid fee")
900 if self.wallet.use_encryption:
901 password = password_dialog(self.window)
908 tx = self.wallet.mktx( to_address, amount, label, password, fee )
909 except BaseException, e:
910 self.show_message(str(e))
913 status, msg = self.wallet.sendtx( tx )
915 self.show_message( "payment sent.\n" + msg )
916 payto_entry.set_text("")
917 label_entry.set_text("")
918 amount_entry.set_text("")
919 fee_entry.set_text("")
921 self.update_sending_tab()
923 self.show_message( msg )
926 def treeview_button_press(self, treeview, event):
927 if event.type == gtk.gdk._2BUTTON_PRESS:
928 c = treeview.get_cursor()[0]
929 if treeview == self.history_treeview:
930 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
931 self.show_message(tx_details)
932 elif treeview == self.contacts_treeview:
933 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
934 a = self.wallet.aliases.get(m)
936 if a[0] in self.wallet.authorities.keys():
937 s = self.wallet.authorities.get(a[0])
940 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
941 self.show_message(msg)
944 def treeview_key_press(self, treeview, event):
945 c = treeview.get_cursor()[0]
946 if event.keyval == gtk.keysyms.Up:
948 treeview.parent.grab_focus()
949 treeview.set_cursor((0,))
950 elif event.keyval == gtk.keysyms.Return:
951 if treeview == self.history_treeview:
952 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
953 self.show_message(tx_details)
954 elif treeview == self.contacts_treeview:
955 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
956 a = self.wallet.aliases.get(m)
958 if a[0] in self.wallet.authorities.keys():
959 s = self.wallet.authorities.get(a[0])
962 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
963 self.show_message(msg)
967 def create_history_tab(self):
969 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
970 treeview = gtk.TreeView(model=self.history_list)
971 self.history_treeview = treeview
972 treeview.set_tooltip_column(7)
974 treeview.connect('key-press-event', self.treeview_key_press)
975 treeview.connect('button-press-event', self.treeview_button_press)
977 tvcolumn = gtk.TreeViewColumn('')
978 treeview.append_column(tvcolumn)
979 cell = gtk.CellRendererPixbuf()
980 tvcolumn.pack_start(cell, False)
981 tvcolumn.set_attributes(cell, stock_id=1)
983 tvcolumn = gtk.TreeViewColumn('Date')
984 treeview.append_column(tvcolumn)
985 cell = gtk.CellRendererText()
986 tvcolumn.pack_start(cell, False)
987 tvcolumn.add_attribute(cell, 'text', 2)
989 tvcolumn = gtk.TreeViewColumn('Description')
990 treeview.append_column(tvcolumn)
991 cell = gtk.CellRendererText()
992 cell.set_property('foreground', 'grey')
993 cell.set_property('family', MONOSPACE_FONT)
994 cell.set_property('editable', True)
995 def edited_cb(cell, path, new_text, h_list):
996 tx = h_list.get_value( h_list.get_iter(path), 0)
997 self.wallet.labels[tx] = new_text
999 self.update_history_tab()
1000 cell.connect('edited', edited_cb, self.history_list)
1001 def editing_started(cell, entry, path, h_list):
1002 tx = h_list.get_value( h_list.get_iter(path), 0)
1003 if not self.wallet.labels.get(tx): entry.set_text('')
1004 cell.connect('editing-started', editing_started, self.history_list)
1005 tvcolumn.set_expand(True)
1006 tvcolumn.pack_start(cell, True)
1007 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
1009 tvcolumn = gtk.TreeViewColumn('Amount')
1010 treeview.append_column(tvcolumn)
1011 cell = gtk.CellRendererText()
1012 cell.set_alignment(1, 0.5)
1013 cell.set_property('family', MONOSPACE_FONT)
1014 tvcolumn.pack_start(cell, False)
1015 tvcolumn.add_attribute(cell, 'text', 5)
1017 tvcolumn = gtk.TreeViewColumn('Balance')
1018 treeview.append_column(tvcolumn)
1019 cell = gtk.CellRendererText()
1020 cell.set_alignment(1, 0.5)
1021 cell.set_property('family', MONOSPACE_FONT)
1022 tvcolumn.pack_start(cell, False)
1023 tvcolumn.add_attribute(cell, 'text', 6)
1025 tvcolumn = gtk.TreeViewColumn('Tooltip')
1026 treeview.append_column(tvcolumn)
1027 cell = gtk.CellRendererText()
1028 tvcolumn.pack_start(cell, False)
1029 tvcolumn.add_attribute(cell, 'text', 7)
1030 tvcolumn.set_visible(False)
1032 scroll = gtk.ScrolledWindow()
1033 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1034 scroll.add(treeview)
1036 self.add_tab(scroll, 'History')
1037 self.update_history_tab()
1040 def create_recv_tab(self):
1041 self.recv_list = gtk.ListStore(str, str, str)
1042 self.add_tab( self.make_address_list(True), 'Receive')
1043 self.update_receiving_tab()
1045 def create_book_tab(self):
1046 self.addressbook_list = gtk.ListStore(str, str, str)
1047 self.add_tab( self.make_address_list(False), 'Contacts')
1048 self.update_sending_tab()
1050 def make_address_list(self, is_recv):
1051 liststore = self.recv_list if is_recv else self.addressbook_list
1052 treeview = gtk.TreeView(model= liststore)
1053 treeview.connect('key-press-event', self.treeview_key_press)
1054 treeview.connect('button-press-event', self.treeview_button_press)
1057 self.contacts_treeview = treeview
1059 tvcolumn = gtk.TreeViewColumn('Address')
1060 treeview.append_column(tvcolumn)
1061 cell = gtk.CellRendererText()
1062 cell.set_property('family', MONOSPACE_FONT)
1063 tvcolumn.pack_start(cell, True)
1064 tvcolumn.add_attribute(cell, 'text', 0)
1066 tvcolumn = gtk.TreeViewColumn('Label')
1067 tvcolumn.set_expand(True)
1068 treeview.append_column(tvcolumn)
1069 cell = gtk.CellRendererText()
1070 cell.set_property('editable', True)
1071 def edited_cb2(cell, path, new_text, liststore):
1072 address = liststore.get_value( liststore.get_iter(path), 0)
1073 self.wallet.labels[address] = new_text
1075 self.wallet.update_tx_labels()
1076 self.update_receiving_tab()
1077 self.update_sending_tab()
1078 self.update_history_tab()
1079 cell.connect('edited', edited_cb2, liststore)
1080 tvcolumn.pack_start(cell, True)
1081 tvcolumn.add_attribute(cell, 'text', 1)
1083 tvcolumn = gtk.TreeViewColumn('Tx')
1084 treeview.append_column(tvcolumn)
1085 cell = gtk.CellRendererText()
1086 tvcolumn.pack_start(cell, True)
1087 tvcolumn.add_attribute(cell, 'text', 2)
1089 scroll = gtk.ScrolledWindow()
1090 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1091 scroll.add(treeview)
1095 button = gtk.Button("New")
1096 button.connect("clicked", self.newaddress_dialog)
1098 hbox.pack_start(button,False)
1100 def showqrcode(w, treeview, liststore):
1101 path, col = treeview.get_cursor()
1103 address = liststore.get_value(liststore.get_iter(path), 0)
1104 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1108 size = qr.getModuleCount()*boxsize
1109 def area_expose_cb(area, event):
1110 style = area.get_style()
1111 k = qr.getModuleCount()
1114 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1115 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1116 area = gtk.DrawingArea()
1117 area.set_size_request(size, size)
1118 area.connect("expose-event", area_expose_cb)
1120 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1121 dialog.vbox.add(area)
1125 button = gtk.Button("QR")
1126 button.connect("clicked", showqrcode, treeview, liststore)
1128 hbox.pack_start(button,False)
1130 button = gtk.Button("Copy to clipboard")
1131 def copy2clipboard(w, treeview, liststore):
1133 path, col = treeview.get_cursor()
1135 address = liststore.get_value( liststore.get_iter(path), 0)
1136 if platform.system() == 'Windows':
1137 from Tkinter import Tk
1141 r.clipboard_append( address )
1144 c = gtk.clipboard_get()
1145 c.set_text( address )
1146 button.connect("clicked", copy2clipboard, treeview, liststore)
1148 hbox.pack_start(button,False)
1151 button = gtk.Button("Pay to")
1152 def payto(w, treeview, liststore):
1153 path, col = treeview.get_cursor()
1155 address = liststore.get_value( liststore.get_iter(path), 0)
1156 self.payto_entry.set_text( address )
1157 self.notebook.set_current_page(1)
1158 self.amount_entry.grab_focus()
1160 button.connect("clicked", payto, treeview, liststore)
1162 hbox.pack_start(button,False)
1165 vbox.pack_start(scroll,True)
1166 vbox.pack_start(hbox, False)
1169 def update_status_bar(self):
1170 interface = self.wallet.interface
1171 if self.funds_error:
1172 text = "Not enough funds"
1173 elif interface and interface.is_connected:
1174 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1175 if not self.wallet.up_to_date:
1176 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1177 text = "Synchronizing..."
1179 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1180 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1181 c, u = self.wallet.get_balance()
1182 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1183 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1185 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1186 self.network_button.set_tooltip_text("Not connected.")
1187 text = "Not connected"
1189 self.status_bar.pop(self.context_id)
1190 self.status_bar.push(self.context_id, text)
1192 if self.wallet.up_to_date and self.wallet_updated:
1193 self.update_history_tab()
1194 self.update_receiving_tab()
1195 # addressbook too...
1196 self.info.set_text( self.wallet.banner )
1197 self.wallet_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.verifier.get_confirmations(tx_hash)
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):