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 electrum.util import print_error
27 from electrum import is_valid
28 from electrum import mnemonic
31 gtk.gdk.threads_init()
34 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
36 from electrum.util import format_satoshis
37 from electrum.interface import DEFAULT_SERVERS
38 from electrum.bitcoin import MIN_RELAY_TX_FEE
40 def numbify(entry, is_int = False):
41 text = entry.get_text().strip()
43 if not is_int: chars +='.'
44 s = ''.join([i for i in text if i in chars])
49 s = s[:p] + '.' + s[p:p+8]
51 amount = int( Decimal(s) * 100000000 )
65 def show_seed_dialog(wallet, password, parent):
67 show_message("No seed")
70 seed = wallet.decode_seed(password)
72 show_message("Incorrect password")
74 dialog = gtk.MessageDialog(
76 flags = gtk.DIALOG_MODAL,
77 buttons = gtk.BUTTONS_OK,
78 message_format = "Your wallet generation seed is:\n\n" + seed \
79 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
80 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
81 dialog.set_title("Seed")
86 def restore_create_dialog(wallet):
88 # ask if the user wants to create a new wallet, or recover from a seed.
89 # if he wants to recover, and nothing is found, do not create wallet
90 dialog = gtk.Dialog("electrum", parent=None,
91 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
92 buttons= ("create", 0, "restore",1, "cancel",2) )
94 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
96 dialog.vbox.pack_start(label)
101 if r==2: return False
102 return 'restore' if r==1 else 'create'
106 def run_recovery_dialog(wallet):
107 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
108 dialog = gtk.MessageDialog(
110 flags = gtk.DIALOG_MODAL,
111 buttons = gtk.BUTTONS_OK_CANCEL,
112 message_format = message)
115 dialog.set_default_response(gtk.RESPONSE_OK)
117 # ask seed, server and gap in the same dialog
118 seed_box = gtk.HBox()
119 seed_label = gtk.Label('Seed or mnemonic:')
120 seed_label.set_size_request(150,-1)
121 seed_box.pack_start(seed_label, False, False, 10)
123 seed_entry = gtk.Entry()
125 seed_entry.set_size_request(450,-1)
126 seed_box.pack_start(seed_entry, False, False, 10)
127 add_help_button(seed_box, '.')
129 vbox.pack_start(seed_box, False, False, 5)
132 gap_label = gtk.Label('Gap limit:')
133 gap_label.set_size_request(150,10)
135 gap.pack_start(gap_label,False, False, 10)
136 gap_entry = gtk.Entry()
137 gap_entry.set_text("%d"%wallet.gap_limit)
138 gap_entry.connect('changed', numbify, True)
140 gap.pack_start(gap_entry,False,False, 10)
141 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.')
143 vbox.pack_start(gap, False,False, 5)
147 gap = gap_entry.get_text()
148 seed = seed_entry.get_text()
151 if r==gtk.RESPONSE_CANCEL:
157 show_message("error")
163 print_error("Warning: Not hex, trying decode")
164 seed = mnemonic.mn_decode( seed.split(' ') )
166 show_message("no seed")
173 def run_settings_dialog(wallet, parent):
175 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
177 dialog = gtk.MessageDialog(
179 flags = gtk.DIALOG_MODAL,
180 buttons = gtk.BUTTONS_OK_CANCEL,
181 message_format = message)
184 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
186 dialog.set_image(image)
187 dialog.set_title("Settings")
190 dialog.set_default_response(gtk.RESPONSE_OK)
193 fee_entry = gtk.Entry()
194 fee_label = gtk.Label('Transaction fee:')
195 fee_label.set_size_request(150,10)
197 fee.pack_start(fee_label,False, False, 10)
198 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
199 fee_entry.connect('changed', numbify, False)
201 fee.pack_start(fee_entry,False,False, 10)
202 add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001')
204 vbox.pack_start(fee, False,False, 5)
207 nz_entry = gtk.Entry()
208 nz_label = gtk.Label('Display zeros:')
209 nz_label.set_size_request(150,10)
211 nz.pack_start(nz_label,False, False, 10)
212 nz_entry.set_text( str( wallet.num_zeros ))
213 nz_entry.connect('changed', numbify, True)
215 nz.pack_start(nz_entry,False,False, 10)
216 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'")
218 vbox.pack_start(nz, False,False, 5)
222 gui_label = gtk.Label('Default GUI:')
223 gui_label.set_size_request(150,10)
225 gui_box.pack_start(gui_label,False, False, 10)
226 gui_combo = gtk.combo_box_new_text()
227 gui_names = ['lite', 'classic', 'gtk', 'text']
228 for name in gui_names: gui_combo.append_text(name.capitalize())
230 gui_box.pack_start(gui_combo,False, False, 10)
231 gui_combo.set_active( gui_names.index( wallet.config.get("gui","lite")) )
233 add_help_button(gui_box, "Select which GUI mode to use at start up.")
235 vbox.pack_start(gui_box, False,False, 5)
239 fee = fee_entry.get_text()
240 nz = nz_entry.get_text()
241 gui = gui_names[ gui_combo.get_active()]
244 if r==gtk.RESPONSE_CANCEL:
248 fee = int( 100000000 * Decimal(fee) )
250 show_message("error")
258 show_message("error")
260 if wallet.num_zeros != nz:
261 wallet.num_zeros = nz
264 wallet.config.set_key('gui',gui,True)
269 def run_network_dialog( wallet, parent ):
271 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
272 interface = wallet.interface
274 if interface.is_connected:
275 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.verifier.height)
277 status = "Not connected"
280 status = "Please choose a server.\nSelect cancel if you are offline."
282 server = interface.server
283 servers = interface.get_servers()
285 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
286 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
287 dialog.set_title("Server")
288 dialog.set_image(image)
292 host_box = gtk.HBox()
293 host_label = gtk.Label('Connect to:')
294 host_label.set_size_request(100,-1)
296 host_box.pack_start(host_label, False, False, 10)
297 host_entry = gtk.Entry()
298 host_entry.set_size_request(200,-1)
299 host_entry.set_text(server)
301 host_box.pack_start(host_entry, False, False, 10)
302 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)')
306 p_box = gtk.HBox(False, 10)
309 p_label = gtk.Label('Protocol:')
310 p_label.set_size_request(100,-1)
312 p_box.pack_start(p_label, False, False, 10)
314 radio1 = gtk.RadioButton(None, "tcp")
315 p_box.pack_start(radio1, True, True, 0)
317 radio2 = gtk.RadioButton(radio1, "http")
318 p_box.pack_start(radio2, True, True, 0)
322 return unicode(host_entry.get_text()).split(':')
324 def set_button(protocol):
327 elif protocol == 'h':
330 def set_protocol(protocol):
331 host = current_line()[0]
333 if protocol not in pp.keys():
334 protocol = pp.keys()[0]
337 host_entry.set_text( host + ':' + port + ':' + protocol)
339 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
340 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
342 server_list = gtk.ListStore(str)
343 for host in servers.keys():
344 server_list.append([host])
346 treeview = gtk.TreeView(model=server_list)
349 if wallet.interface.servers:
350 label = 'Active Servers'
352 label = 'Default Servers'
354 tvcolumn = gtk.TreeViewColumn(label)
355 treeview.append_column(tvcolumn)
356 cell = gtk.CellRendererText()
357 tvcolumn.pack_start(cell, False)
358 tvcolumn.add_attribute(cell, 'text', 0)
360 scroll = gtk.ScrolledWindow()
361 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
365 vbox.pack_start(host_box, False,False, 5)
366 vbox.pack_start(p_box, True, True, 0)
367 vbox.pack_start(scroll)
369 def my_treeview_cb(treeview):
370 path, view_column = treeview.get_cursor()
371 host = server_list.get_value( server_list.get_iter(path), 0)
377 protocol = pp.keys()[0]
379 host_entry.set_text( host + ':' + port + ':' + protocol)
382 treeview.connect('cursor-changed', my_treeview_cb)
386 server = host_entry.get_text()
389 if r==gtk.RESPONSE_CANCEL:
393 interface.set_server(server)
395 show_message("error:" + server)
399 wallet.config.set_key("server", server, True)
404 def show_message(message, parent=None):
405 dialog = gtk.MessageDialog(
407 flags = gtk.DIALOG_MODAL,
408 buttons = gtk.BUTTONS_CLOSE,
409 message_format = message )
414 def password_line(label):
415 password = gtk.HBox()
416 password_label = gtk.Label(label)
417 password_label.set_size_request(120,10)
418 password_label.show()
419 password.pack_start(password_label,False, False, 10)
420 password_entry = gtk.Entry()
421 password_entry.set_size_request(300,-1)
422 password_entry.set_visibility(False)
423 password_entry.show()
424 password.pack_start(password_entry,False,False, 10)
426 return password, password_entry
428 def password_dialog(parent):
429 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
430 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
431 dialog.get_image().set_visible(False)
432 current_pw, current_pw_entry = password_line('Password:')
433 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
434 dialog.vbox.pack_start(current_pw, False, True, 0)
436 result = dialog.run()
437 pw = current_pw_entry.get_text()
439 if result != gtk.RESPONSE_CANCEL: return pw
441 def change_password_dialog(wallet, parent, icon):
443 show_message("No seed")
447 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'
449 msg = "Please choose a password to encrypt your wallet keys"
451 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
452 dialog.set_title("Change password")
454 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
456 dialog.set_image(image)
458 if wallet.use_encryption:
459 current_pw, current_pw_entry = password_line('Current password:')
460 dialog.vbox.pack_start(current_pw, False, True, 0)
462 password, password_entry = password_line('New password:')
463 dialog.vbox.pack_start(password, False, True, 5)
464 password2, password2_entry = password_line('Confirm password:')
465 dialog.vbox.pack_start(password2, False, True, 5)
468 result = dialog.run()
469 password = current_pw_entry.get_text() if wallet.use_encryption else None
470 new_password = password_entry.get_text()
471 new_password2 = password2_entry.get_text()
473 if result == gtk.RESPONSE_CANCEL:
477 seed = wallet.decode_seed(password)
479 show_message("Incorrect password")
482 if new_password != new_password2:
483 show_message("passwords do not match")
486 wallet.update_password(seed, password, new_password)
489 if wallet.use_encryption:
490 icon.set_tooltip_text('wallet is encrypted')
492 icon.set_tooltip_text('wallet is unencrypted')
495 def add_help_button(hbox, message):
496 button = gtk.Button('?')
497 button.connect("clicked", lambda x: show_message(message))
499 hbox.pack_start(button,False, False)
502 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
504 gobject.type_register(MyWindow)
505 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
506 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
509 class ElectrumWindow:
511 def show_message(self, msg):
512 show_message(msg, self.window)
514 def __init__(self, wallet, config):
517 self.funds_error = False # True if not enough funds
519 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
520 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
521 if not self.wallet.seed: title += ' [seedless]'
522 self.window.set_title(title)
523 self.window.connect("destroy", gtk.main_quit)
524 self.window.set_border_width(0)
525 self.window.connect('mykeypress', gtk.main_quit)
526 self.window.set_default_size(720, 350)
527 self.wallet_updated = False
531 self.notebook = gtk.Notebook()
532 self.create_history_tab()
534 self.create_send_tab()
535 self.create_recv_tab()
536 self.create_book_tab()
537 self.create_about_tab()
539 vbox.pack_start(self.notebook, True, True, 2)
541 self.status_bar = gtk.Statusbar()
542 vbox.pack_start(self.status_bar, False, False, 0)
544 self.status_image = gtk.Image()
545 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
546 self.status_image.set_alignment(True, 0.5 )
547 self.status_image.show()
549 self.network_button = gtk.Button()
550 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
551 self.network_button.add(self.status_image)
552 self.network_button.set_relief(gtk.RELIEF_NONE)
553 self.network_button.show()
554 self.status_bar.pack_end(self.network_button, False, False)
557 def seedb(w, wallet):
558 if wallet.use_encryption:
559 password = password_dialog(self.window)
560 if not password: return
561 else: password = None
562 show_seed_dialog(wallet, password, self.window)
563 button = gtk.Button('S')
564 button.connect("clicked", seedb, wallet )
565 button.set_relief(gtk.RELIEF_NONE)
567 self.status_bar.pack_end(button,False, False)
569 settings_icon = gtk.Image()
570 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
571 settings_icon.set_alignment(0.5, 0.5)
572 settings_icon.set_size_request(16,16 )
575 prefs_button = gtk.Button()
576 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
577 prefs_button.add(settings_icon)
578 prefs_button.set_tooltip_text("Settings")
579 prefs_button.set_relief(gtk.RELIEF_NONE)
581 self.status_bar.pack_end(prefs_button,False,False)
583 pw_icon = gtk.Image()
584 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
585 pw_icon.set_alignment(0.5, 0.5)
586 pw_icon.set_size_request(16,16 )
590 password_button = gtk.Button()
591 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
592 password_button.add(pw_icon)
593 password_button.set_relief(gtk.RELIEF_NONE)
594 password_button.show()
595 self.status_bar.pack_end(password_button,False,False)
597 self.window.add(vbox)
598 self.window.show_all()
601 self.context_id = self.status_bar.get_context_id("statusbar")
602 self.update_status_bar()
604 self.wallet.interface.register_callback('updated', self.update_callback)
607 def update_status_bar_thread():
609 gobject.idle_add( self.update_status_bar )
613 def check_recipient_thread():
617 if self.payto_entry.is_focus():
619 r = self.payto_entry.get_text()
623 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
625 to_address = self.wallet.get_alias(r, interactive=False)
629 s = r + ' <' + to_address + '>'
630 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
633 thread.start_new_thread(update_status_bar_thread, ())
635 thread.start_new_thread(check_recipient_thread, ())
636 self.notebook.set_current_page(0)
638 def update_callback(self):
639 self.wallet_updated = True
642 def add_tab(self, page, name):
643 tab_label = gtk.Label(name)
645 self.notebook.append_page(page, tab_label)
648 def create_send_tab(self):
650 page = vbox = gtk.VBox()
654 payto_label = gtk.Label('Pay to:')
655 payto_label.set_size_request(100,-1)
656 payto.pack_start(payto_label, False)
657 payto_entry = gtk.Entry()
658 payto_entry.set_size_request(450, 26)
659 payto.pack_start(payto_entry, False)
660 vbox.pack_start(payto, False, False, 5)
663 message_label = gtk.Label('Description:')
664 message_label.set_size_request(100,-1)
665 message.pack_start(message_label, False)
666 message_entry = gtk.Entry()
667 message_entry.set_size_request(450, 26)
668 message.pack_start(message_entry, False)
669 vbox.pack_start(message, False, False, 5)
671 amount_box = gtk.HBox()
672 amount_label = gtk.Label('Amount:')
673 amount_label.set_size_request(100,-1)
674 amount_box.pack_start(amount_label, False)
675 amount_entry = gtk.Entry()
676 amount_entry.set_size_request(120, -1)
677 amount_box.pack_start(amount_entry, False)
678 vbox.pack_start(amount_box, False, False, 5)
680 self.fee_box = fee_box = gtk.HBox()
681 fee_label = gtk.Label('Fee:')
682 fee_label.set_size_request(100,-1)
683 fee_box.pack_start(fee_label, False)
684 fee_entry = gtk.Entry()
685 fee_entry.set_size_request(60, 26)
686 fee_box.pack_start(fee_entry, False)
687 vbox.pack_start(fee_box, False, False, 5)
690 empty_label = gtk.Label('')
691 empty_label.set_size_request(100,-1)
692 end_box.pack_start(empty_label, False)
693 send_button = gtk.Button("Send")
695 end_box.pack_start(send_button, False, False, 0)
696 clear_button = gtk.Button("Clear")
698 end_box.pack_start(clear_button, False, False, 15)
699 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
700 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
702 vbox.pack_start(end_box, False, False, 5)
704 # display this line only if there is a signature
705 payto_sig = gtk.HBox()
706 payto_sig_id = gtk.Label('')
707 payto_sig.pack_start(payto_sig_id, False)
708 vbox.pack_start(payto_sig, True, True, 5)
711 self.user_fee = False
713 def entry_changed( entry, is_fee ):
714 self.funds_error = False
715 amount = numbify(amount_entry)
716 fee = numbify(fee_entry)
717 if not is_fee: fee = None
720 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
722 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
725 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
726 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
727 send_button.set_sensitive(True)
729 send_button.set_sensitive(False)
730 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
731 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
732 self.funds_error = True
734 amount_entry.connect('changed', entry_changed, False)
735 fee_entry.connect('changed', entry_changed, True)
737 self.payto_entry = payto_entry
738 self.payto_fee_entry = fee_entry
739 self.payto_sig_id = payto_sig_id
740 self.payto_sig = payto_sig
741 self.amount_entry = amount_entry
742 self.message_entry = message_entry
743 self.add_tab(page, 'Send')
745 def set_frozen(self,entry,frozen):
747 entry.set_editable(False)
748 entry.set_has_frame(False)
749 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
751 entry.set_editable(True)
752 entry.set_has_frame(True)
753 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
755 def set_url(self, url):
756 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
757 self.notebook.set_current_page(1)
758 self.payto_entry.set_text(payto)
759 self.message_entry.set_text(message)
760 self.amount_entry.set_text(amount)
762 self.set_frozen(self.payto_entry,True)
763 self.set_frozen(self.amount_entry,True)
764 self.set_frozen(self.message_entry,True)
765 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
767 self.payto_sig.set_visible(False)
769 def create_about_tab(self):
774 tv.set_editable(False)
775 tv.set_cursor_visible(False)
776 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
777 scroll = gtk.ScrolledWindow()
779 page.pack_start(scroll)
780 self.info = tv.get_buffer()
781 self.add_tab(page, 'Wall')
783 def do_clear(self, w, data):
784 self.payto_sig.set_visible(False)
785 self.payto_fee_entry.set_text('')
786 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
787 self.set_frozen(entry,False)
790 def question(self,msg):
791 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
793 result = dialog.run()
795 return result == gtk.RESPONSE_OK
797 def do_send(self, w, data):
798 payto_entry, label_entry, amount_entry, fee_entry = data
799 label = label_entry.get_text()
800 r = payto_entry.get_text()
803 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
804 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
807 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
811 self.update_sending_tab()
814 to_address = m2.group(5)
818 if not is_valid(to_address):
819 self.show_message( "invalid bitcoin address:\n"+to_address)
823 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
825 self.show_message( "invalid amount")
828 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
830 self.show_message( "invalid fee")
833 if self.wallet.use_encryption:
834 password = password_dialog(self.window)
841 tx = self.wallet.mktx( [(to_address, amount)], password, fee )
842 except BaseException, e:
843 self.show_message(str(e))
846 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
847 self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." )
852 self.wallet.labels[tx.hash()] = label
854 status, msg = self.wallet.sendtx( tx )
856 self.show_message( "payment sent.\n" + msg )
857 payto_entry.set_text("")
858 label_entry.set_text("")
859 amount_entry.set_text("")
860 fee_entry.set_text("")
862 self.update_sending_tab()
864 self.show_message( msg )
867 def treeview_button_press(self, treeview, event):
868 if event.type == gtk.gdk._2BUTTON_PRESS:
869 c = treeview.get_cursor()[0]
870 if treeview == self.history_treeview:
871 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
872 self.show_message(tx_details)
873 elif treeview == self.contacts_treeview:
874 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
875 #a = self.wallet.aliases.get(m)
877 # if a[0] in self.wallet.authorities.keys():
878 # s = self.wallet.authorities.get(a[0])
881 # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
882 # self.show_message(msg)
885 def treeview_key_press(self, treeview, event):
886 c = treeview.get_cursor()[0]
887 if event.keyval == gtk.keysyms.Up:
889 treeview.parent.grab_focus()
890 treeview.set_cursor((0,))
891 elif event.keyval == gtk.keysyms.Return:
892 if treeview == self.history_treeview:
893 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
894 self.show_message(tx_details)
895 elif treeview == self.contacts_treeview:
896 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
897 #a = self.wallet.aliases.get(m)
899 # if a[0] in self.wallet.authorities.keys():
900 # s = self.wallet.authorities.get(a[0])
903 # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
904 # self.show_message(msg)
908 def create_history_tab(self):
910 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
911 treeview = gtk.TreeView(model=self.history_list)
912 self.history_treeview = treeview
913 treeview.set_tooltip_column(7)
915 treeview.connect('key-press-event', self.treeview_key_press)
916 treeview.connect('button-press-event', self.treeview_button_press)
918 tvcolumn = gtk.TreeViewColumn('')
919 treeview.append_column(tvcolumn)
920 cell = gtk.CellRendererPixbuf()
921 tvcolumn.pack_start(cell, False)
922 tvcolumn.set_attributes(cell, stock_id=1)
924 tvcolumn = gtk.TreeViewColumn('Date')
925 treeview.append_column(tvcolumn)
926 cell = gtk.CellRendererText()
927 tvcolumn.pack_start(cell, False)
928 tvcolumn.add_attribute(cell, 'text', 2)
930 tvcolumn = gtk.TreeViewColumn('Description')
931 treeview.append_column(tvcolumn)
932 cell = gtk.CellRendererText()
933 cell.set_property('foreground', 'grey')
934 cell.set_property('family', MONOSPACE_FONT)
935 cell.set_property('editable', True)
936 def edited_cb(cell, path, new_text, h_list):
937 tx = h_list.get_value( h_list.get_iter(path), 0)
938 self.wallet.labels[tx] = new_text
940 self.update_history_tab()
941 cell.connect('edited', edited_cb, self.history_list)
942 def editing_started(cell, entry, path, h_list):
943 tx = h_list.get_value( h_list.get_iter(path), 0)
944 if not self.wallet.labels.get(tx): entry.set_text('')
945 cell.connect('editing-started', editing_started, self.history_list)
946 tvcolumn.set_expand(True)
947 tvcolumn.pack_start(cell, True)
948 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
950 tvcolumn = gtk.TreeViewColumn('Amount')
951 treeview.append_column(tvcolumn)
952 cell = gtk.CellRendererText()
953 cell.set_alignment(1, 0.5)
954 cell.set_property('family', MONOSPACE_FONT)
955 tvcolumn.pack_start(cell, False)
956 tvcolumn.add_attribute(cell, 'text', 5)
958 tvcolumn = gtk.TreeViewColumn('Balance')
959 treeview.append_column(tvcolumn)
960 cell = gtk.CellRendererText()
961 cell.set_alignment(1, 0.5)
962 cell.set_property('family', MONOSPACE_FONT)
963 tvcolumn.pack_start(cell, False)
964 tvcolumn.add_attribute(cell, 'text', 6)
966 tvcolumn = gtk.TreeViewColumn('Tooltip')
967 treeview.append_column(tvcolumn)
968 cell = gtk.CellRendererText()
969 tvcolumn.pack_start(cell, False)
970 tvcolumn.add_attribute(cell, 'text', 7)
971 tvcolumn.set_visible(False)
973 scroll = gtk.ScrolledWindow()
974 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
977 self.add_tab(scroll, 'History')
978 self.update_history_tab()
981 def create_recv_tab(self):
982 self.recv_list = gtk.ListStore(str, str, str)
983 self.add_tab( self.make_address_list(True), 'Receive')
984 self.update_receiving_tab()
986 def create_book_tab(self):
987 self.addressbook_list = gtk.ListStore(str, str, str)
988 self.add_tab( self.make_address_list(False), 'Contacts')
989 self.update_sending_tab()
991 def make_address_list(self, is_recv):
992 liststore = self.recv_list if is_recv else self.addressbook_list
993 treeview = gtk.TreeView(model= liststore)
994 treeview.connect('key-press-event', self.treeview_key_press)
995 treeview.connect('button-press-event', self.treeview_button_press)
998 self.contacts_treeview = treeview
1000 tvcolumn = gtk.TreeViewColumn('Address')
1001 treeview.append_column(tvcolumn)
1002 cell = gtk.CellRendererText()
1003 cell.set_property('family', MONOSPACE_FONT)
1004 tvcolumn.pack_start(cell, True)
1005 tvcolumn.add_attribute(cell, 'text', 0)
1007 tvcolumn = gtk.TreeViewColumn('Label')
1008 tvcolumn.set_expand(True)
1009 treeview.append_column(tvcolumn)
1010 cell = gtk.CellRendererText()
1011 cell.set_property('editable', True)
1012 def edited_cb2(cell, path, new_text, liststore):
1013 address = liststore.get_value( liststore.get_iter(path), 0)
1014 self.wallet.labels[address] = new_text
1016 self.update_receiving_tab()
1017 self.update_sending_tab()
1018 self.update_history_tab()
1019 cell.connect('edited', edited_cb2, liststore)
1020 tvcolumn.pack_start(cell, True)
1021 tvcolumn.add_attribute(cell, 'text', 1)
1023 tvcolumn = gtk.TreeViewColumn('Tx')
1024 treeview.append_column(tvcolumn)
1025 cell = gtk.CellRendererText()
1026 tvcolumn.pack_start(cell, True)
1027 tvcolumn.add_attribute(cell, 'text', 2)
1029 scroll = gtk.ScrolledWindow()
1030 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1031 scroll.add(treeview)
1035 button = gtk.Button("New")
1036 button.connect("clicked", self.newaddress_dialog)
1038 hbox.pack_start(button,False)
1040 def showqrcode(w, treeview, liststore):
1041 path, col = treeview.get_cursor()
1043 address = liststore.get_value(liststore.get_iter(path), 0)
1044 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1048 size = qr.getModuleCount()*boxsize
1049 def area_expose_cb(area, event):
1050 style = area.get_style()
1051 k = qr.getModuleCount()
1054 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1055 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1056 area = gtk.DrawingArea()
1057 area.set_size_request(size, size)
1058 area.connect("expose-event", area_expose_cb)
1060 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1061 dialog.vbox.add(area)
1065 button = gtk.Button("QR")
1066 button.connect("clicked", showqrcode, treeview, liststore)
1068 hbox.pack_start(button,False)
1070 button = gtk.Button("Copy to clipboard")
1071 def copy2clipboard(w, treeview, liststore):
1073 path, col = treeview.get_cursor()
1075 address = liststore.get_value( liststore.get_iter(path), 0)
1076 if platform.system() == 'Windows':
1077 from Tkinter import Tk
1081 r.clipboard_append( address )
1084 c = gtk.clipboard_get()
1085 c.set_text( address )
1086 button.connect("clicked", copy2clipboard, treeview, liststore)
1088 hbox.pack_start(button,False)
1091 button = gtk.Button("Pay to")
1092 def payto(w, treeview, liststore):
1093 path, col = treeview.get_cursor()
1095 address = liststore.get_value( liststore.get_iter(path), 0)
1096 self.payto_entry.set_text( address )
1097 self.notebook.set_current_page(1)
1098 self.amount_entry.grab_focus()
1100 button.connect("clicked", payto, treeview, liststore)
1102 hbox.pack_start(button,False)
1105 vbox.pack_start(scroll,True)
1106 vbox.pack_start(hbox, False)
1109 def update_status_bar(self):
1110 interface = self.wallet.interface
1111 if self.funds_error:
1112 text = "Not enough funds"
1113 elif interface and interface.is_connected:
1114 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1115 if not self.wallet.up_to_date:
1116 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1117 text = "Synchronizing..."
1119 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1120 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1121 c, u = self.wallet.get_balance()
1122 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1123 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1125 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1126 self.network_button.set_tooltip_text("Not connected.")
1127 text = "Not connected"
1129 self.status_bar.pop(self.context_id)
1130 self.status_bar.push(self.context_id, text)
1132 if self.wallet.up_to_date and self.wallet_updated:
1133 self.update_history_tab()
1134 self.update_receiving_tab()
1135 # addressbook too...
1136 self.info.set_text( self.wallet.interface.banner )
1137 self.wallet_updated = False
1139 def update_receiving_tab(self):
1140 self.recv_list.clear()
1141 for address in self.wallet.addresses(True):
1142 if self.wallet.is_change(address):continue
1143 label = self.wallet.labels.get(address)
1144 h = self.wallet.history.get(address,[])
1146 tx = "None" if n==0 else "%d"%n
1147 self.recv_list.append((address, label, tx ))
1149 def update_sending_tab(self):
1150 # detect addresses that are not mine in history, add them here...
1151 self.addressbook_list.clear()
1152 #for alias, v in self.wallet.aliases.items():
1154 # label = self.wallet.labels.get(alias)
1155 # self.addressbook_list.append((alias, label, '-'))
1157 for address in self.wallet.addressbook:
1158 label = self.wallet.labels.get(address)
1159 n = self.wallet.get_num_tx(address)
1160 self.addressbook_list.append((address, label, "%d"%n))
1162 def update_history_tab(self):
1163 cursor = self.history_treeview.get_cursor()[0]
1164 self.history_list.clear()
1166 for item in self.wallet.get_tx_history():
1167 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1170 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1173 conf_icon = gtk.STOCK_APPLY
1175 time_str = 'unverified'
1178 time_str = 'pending'
1179 conf_icon = gtk.STOCK_EXECUTE
1181 label, is_default_label = self.wallet.get_label(tx_hash)
1182 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1183 details = self.get_tx_details(tx_hash)
1185 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1186 format_satoshis(value,True,self.wallet.num_zeros),
1187 format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1188 if cursor: self.history_treeview.set_cursor( cursor )
1191 def get_tx_details(self, tx_hash):
1193 if not tx_hash: return ''
1194 tx = self.wallet.transactions.get(tx_hash)
1195 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
1196 conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1199 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1201 time_str = 'pending'
1203 inputs = map(lambda x: x.get('address'), tx.inputs)
1204 outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1205 tx_details = "Transaction Details" +"\n\n" \
1206 + "Transaction ID:\n" + tx_hash + "\n\n" \
1207 + "Status: %d confirmations\n"%conf
1210 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1211 + "Transaction fee: %s\n"% format_satoshis(fee, False)
1213 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1214 + "Transaction fee: unknown\n"
1216 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1218 tx_details += "Date: %s\n\n"%time_str \
1219 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1220 + "Outputs:\n-"+ '\n-'.join(outputs)
1226 def newaddress_dialog(self, w):
1228 title = "New Contact"
1229 dialog = gtk.Dialog(title, parent=self.window,
1230 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1231 buttons= ("cancel", 0, "ok",1) )
1235 label_label = gtk.Label('Label:')
1236 label_label.set_size_request(120,10)
1238 label.pack_start(label_label)
1239 label_entry = gtk.Entry()
1241 label.pack_start(label_entry)
1243 dialog.vbox.pack_start(label, False, True, 5)
1245 address = gtk.HBox()
1246 address_label = gtk.Label('Address:')
1247 address_label.set_size_request(120,10)
1248 address_label.show()
1249 address.pack_start(address_label)
1250 address_entry = gtk.Entry()
1251 address_entry.show()
1252 address.pack_start(address_entry)
1254 dialog.vbox.pack_start(address, False, True, 5)
1256 result = dialog.run()
1257 address = address_entry.get_text()
1258 label = label_entry.get_text()
1262 if is_valid(address):
1263 self.wallet.addressbook.append(address)
1264 if label: self.wallet.labels[address] = label
1266 self.update_sending_tab()
1268 errorDialog = gtk.MessageDialog(
1270 flags=gtk.DIALOG_MODAL,
1271 buttons= gtk.BUTTONS_CLOSE,
1272 message_format = "Invalid address")
1275 errorDialog.destroy()
1279 class ElectrumGui():
1281 def __init__(self, wallet, config):
1282 self.wallet = wallet
1283 self.config = config
1285 def main(self, url=None):
1286 ew = ElectrumWindow(self.wallet, self.config)
1287 if url: ew.set_url(url)
1290 def restore_or_create(self):
1291 return restore_create_dialog(self.wallet)
1293 def seed_dialog(self):
1294 return run_recovery_dialog( self.wallet )
1296 def verify_seed(self):
1297 self.wallet.save_seed()
1300 def network_dialog(self):
1301 return run_network_dialog( self.wallet, parent=None )
1303 def show_seed(self):
1304 show_seed_dialog(self.wallet, None, None)
1306 def password_dialog(self):
1307 change_password_dialog(self.wallet, None, None)
1309 def restore_wallet(self):
1310 wallet = self.wallet
1312 dialog = gtk.MessageDialog(
1314 flags = gtk.DIALOG_MODAL,
1315 buttons = gtk.BUTTONS_CANCEL,
1316 message_format = "Please wait..." )
1319 def recover_thread( wallet, dialog ):
1320 while not wallet.is_up_to_date():
1322 gobject.idle_add( dialog.destroy )
1324 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1327 if r==gtk.RESPONSE_CANCEL: return False
1328 if not wallet.is_found():
1329 show_message("No transactions found for this seed")