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
39 def numbify(entry, is_int = False):
40 text = entry.get_text().strip()
42 if not is_int: chars +='.'
43 s = ''.join([i for i in text if i in chars])
48 s = s[:p] + '.' + s[p:p+8]
50 amount = int( Decimal(s) * 100000000 )
64 def show_seed_dialog(wallet, password, parent):
66 show_message("No seed")
69 seed = wallet.decode_seed(password)
71 show_message("Incorrect password")
73 dialog = gtk.MessageDialog(
75 flags = gtk.DIALOG_MODAL,
76 buttons = gtk.BUTTONS_OK,
77 message_format = "Your wallet generation seed is:\n\n" + seed \
78 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
79 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
80 dialog.set_title("Seed")
85 def restore_create_dialog(wallet):
87 # ask if the user wants to create a new wallet, or recover from a seed.
88 # if he wants to recover, and nothing is found, do not create wallet
89 dialog = gtk.Dialog("electrum", parent=None,
90 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
91 buttons= ("create", 0, "restore",1, "cancel",2) )
93 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
95 dialog.vbox.pack_start(label)
100 if r==2: return False
101 return 'restore' if r==1 else 'create'
105 def run_recovery_dialog(wallet):
106 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
107 dialog = gtk.MessageDialog(
109 flags = gtk.DIALOG_MODAL,
110 buttons = gtk.BUTTONS_OK_CANCEL,
111 message_format = message)
114 dialog.set_default_response(gtk.RESPONSE_OK)
116 # ask seed, server and gap in the same dialog
117 seed_box = gtk.HBox()
118 seed_label = gtk.Label('Seed or mnemonic:')
119 seed_label.set_size_request(150,-1)
120 seed_box.pack_start(seed_label, False, False, 10)
122 seed_entry = gtk.Entry()
124 seed_entry.set_size_request(450,-1)
125 seed_box.pack_start(seed_entry, False, False, 10)
126 add_help_button(seed_box, '.')
128 vbox.pack_start(seed_box, False, False, 5)
131 gap_label = gtk.Label('Gap limit:')
132 gap_label.set_size_request(150,10)
134 gap.pack_start(gap_label,False, False, 10)
135 gap_entry = gtk.Entry()
136 gap_entry.set_text("%d"%wallet.gap_limit)
137 gap_entry.connect('changed', numbify, True)
139 gap.pack_start(gap_entry,False,False, 10)
140 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.')
142 vbox.pack_start(gap, False,False, 5)
146 gap = gap_entry.get_text()
147 seed = seed_entry.get_text()
150 if r==gtk.RESPONSE_CANCEL:
156 show_message("error")
162 print_error("Warning: Not hex, trying decode")
163 seed = mnemonic.mn_decode( seed.split(' ') )
165 show_message("no seed")
172 def run_settings_dialog(wallet, parent):
174 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
176 dialog = gtk.MessageDialog(
178 flags = gtk.DIALOG_MODAL,
179 buttons = gtk.BUTTONS_OK_CANCEL,
180 message_format = message)
183 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
185 dialog.set_image(image)
186 dialog.set_title("Settings")
189 dialog.set_default_response(gtk.RESPONSE_OK)
192 fee_entry = gtk.Entry()
193 fee_label = gtk.Label('Transaction fee:')
194 fee_label.set_size_request(150,10)
196 fee.pack_start(fee_label,False, False, 10)
197 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
198 fee_entry.connect('changed', numbify, False)
200 fee.pack_start(fee_entry,False,False, 10)
201 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
203 vbox.pack_start(fee, False,False, 5)
206 nz_entry = gtk.Entry()
207 nz_label = gtk.Label('Display zeros:')
208 nz_label.set_size_request(150,10)
210 nz.pack_start(nz_label,False, False, 10)
211 nz_entry.set_text( str( wallet.num_zeros ))
212 nz_entry.connect('changed', numbify, True)
214 nz.pack_start(nz_entry,False,False, 10)
215 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'")
217 vbox.pack_start(nz, False,False, 5)
221 gui_label = gtk.Label('Default GUI:')
222 gui_label.set_size_request(150,10)
224 gui_box.pack_start(gui_label,False, False, 10)
225 gui_combo = gtk.combo_box_new_text()
226 gui_names = ['lite', 'classic', 'gtk', 'text']
227 for name in gui_names: gui_combo.append_text(name.capitalize())
229 gui_box.pack_start(gui_combo,False, False, 10)
230 gui_combo.set_active( gui_names.index( wallet.config.get("gui","lite")) )
232 add_help_button(gui_box, "Select which GUI mode to use at start up.")
234 vbox.pack_start(gui_box, False,False, 5)
238 fee = fee_entry.get_text()
239 nz = nz_entry.get_text()
240 gui = gui_names[ gui_combo.get_active()]
243 if r==gtk.RESPONSE_CANCEL:
247 fee = int( 100000000 * Decimal(fee) )
249 show_message("error")
251 if wallet.fee != fee:
259 show_message("error")
261 if wallet.num_zeros != nz:
262 wallet.num_zeros = nz
265 wallet.config.set_key('gui',gui,True)
270 def run_network_dialog( wallet, parent ):
272 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
273 interface = wallet.interface
275 if interface.is_connected:
276 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.verifier.height)
278 status = "Not connected"
281 status = "Please choose a server.\nSelect cancel if you are offline."
283 server = interface.server
284 plist, servers_list = interface.get_servers_list()
286 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
287 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
288 dialog.set_title("Server")
289 dialog.set_image(image)
293 host_box = gtk.HBox()
294 host_label = gtk.Label('Connect to:')
295 host_label.set_size_request(100,-1)
297 host_box.pack_start(host_label, False, False, 10)
298 host_entry = gtk.Entry()
299 host_entry.set_size_request(200,-1)
300 host_entry.set_text(server)
302 host_box.pack_start(host_entry, False, False, 10)
303 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)')
307 p_box = gtk.HBox(False, 10)
310 p_label = gtk.Label('Protocol:')
311 p_label.set_size_request(100,-1)
313 p_box.pack_start(p_label, False, False, 10)
315 radio1 = gtk.RadioButton(None, "tcp")
316 p_box.pack_start(radio1, True, True, 0)
318 radio2 = gtk.RadioButton(radio1, "http")
319 p_box.pack_start(radio2, True, True, 0)
323 return unicode(host_entry.get_text()).split(':')
325 def set_button(protocol):
328 elif protocol == 'h':
331 def set_protocol(protocol):
332 host = current_line()[0]
334 if protocol not in pp.keys():
335 protocol = pp.keys()[0]
338 host_entry.set_text( host + ':' + port + ':' + protocol)
340 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
341 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
343 server_list = gtk.ListStore(str)
344 for host in plist.keys():
345 server_list.append([host])
347 treeview = gtk.TreeView(model=server_list)
350 if wallet.interface.servers:
351 label = 'Active Servers'
353 label = 'Default Servers'
355 tvcolumn = gtk.TreeViewColumn(label)
356 treeview.append_column(tvcolumn)
357 cell = gtk.CellRendererText()
358 tvcolumn.pack_start(cell, False)
359 tvcolumn.add_attribute(cell, 'text', 0)
361 scroll = gtk.ScrolledWindow()
362 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
366 vbox.pack_start(host_box, False,False, 5)
367 vbox.pack_start(p_box, True, True, 0)
368 vbox.pack_start(scroll)
370 def my_treeview_cb(treeview):
371 path, view_column = treeview.get_cursor()
372 host = server_list.get_value( server_list.get_iter(path), 0)
378 protocol = pp.keys()[0]
380 host_entry.set_text( host + ':' + port + ':' + protocol)
383 treeview.connect('cursor-changed', my_treeview_cb)
387 server = host_entry.get_text()
390 if r==gtk.RESPONSE_CANCEL:
394 interface.set_server(server)
396 show_message("error:" + server)
400 wallet.config.set_key("server", server, True)
405 def show_message(message, parent=None):
406 dialog = gtk.MessageDialog(
408 flags = gtk.DIALOG_MODAL,
409 buttons = gtk.BUTTONS_CLOSE,
410 message_format = message )
415 def password_line(label):
416 password = gtk.HBox()
417 password_label = gtk.Label(label)
418 password_label.set_size_request(120,10)
419 password_label.show()
420 password.pack_start(password_label,False, False, 10)
421 password_entry = gtk.Entry()
422 password_entry.set_size_request(300,-1)
423 password_entry.set_visibility(False)
424 password_entry.show()
425 password.pack_start(password_entry,False,False, 10)
427 return password, password_entry
429 def password_dialog(parent):
430 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
431 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
432 dialog.get_image().set_visible(False)
433 current_pw, current_pw_entry = password_line('Password:')
434 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
435 dialog.vbox.pack_start(current_pw, False, True, 0)
437 result = dialog.run()
438 pw = current_pw_entry.get_text()
440 if result != gtk.RESPONSE_CANCEL: return pw
442 def change_password_dialog(wallet, parent, icon):
444 show_message("No seed")
448 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'
450 msg = "Please choose a password to encrypt your wallet keys"
452 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
453 dialog.set_title("Change password")
455 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
457 dialog.set_image(image)
459 if wallet.use_encryption:
460 current_pw, current_pw_entry = password_line('Current password:')
461 dialog.vbox.pack_start(current_pw, False, True, 0)
463 password, password_entry = password_line('New password:')
464 dialog.vbox.pack_start(password, False, True, 5)
465 password2, password2_entry = password_line('Confirm password:')
466 dialog.vbox.pack_start(password2, False, True, 5)
469 result = dialog.run()
470 password = current_pw_entry.get_text() if wallet.use_encryption else None
471 new_password = password_entry.get_text()
472 new_password2 = password2_entry.get_text()
474 if result == gtk.RESPONSE_CANCEL:
478 seed = wallet.decode_seed(password)
480 show_message("Incorrect password")
483 if new_password != new_password2:
484 show_message("passwords do not match")
487 wallet.update_password(seed, password, new_password)
490 if wallet.use_encryption:
491 icon.set_tooltip_text('wallet is encrypted')
493 icon.set_tooltip_text('wallet is unencrypted')
496 def add_help_button(hbox, message):
497 button = gtk.Button('?')
498 button.connect("clicked", lambda x: show_message(message))
500 hbox.pack_start(button,False, False)
503 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
505 gobject.type_register(MyWindow)
506 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
507 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
510 class ElectrumWindow:
512 def show_message(self, msg):
513 show_message(msg, self.window)
515 def __init__(self, wallet, config):
518 self.funds_error = False # True if not enough funds
520 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
521 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
522 if not self.wallet.seed: title += ' [seedless]'
523 self.window.set_title(title)
524 self.window.connect("destroy", gtk.main_quit)
525 self.window.set_border_width(0)
526 self.window.connect('mykeypress', gtk.main_quit)
527 self.window.set_default_size(720, 350)
528 self.wallet_updated = False
532 self.notebook = gtk.Notebook()
533 self.create_history_tab()
535 self.create_send_tab()
536 self.create_recv_tab()
537 self.create_book_tab()
538 self.create_about_tab()
540 vbox.pack_start(self.notebook, True, True, 2)
542 self.status_bar = gtk.Statusbar()
543 vbox.pack_start(self.status_bar, False, False, 0)
545 self.status_image = gtk.Image()
546 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
547 self.status_image.set_alignment(True, 0.5 )
548 self.status_image.show()
550 self.network_button = gtk.Button()
551 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
552 self.network_button.add(self.status_image)
553 self.network_button.set_relief(gtk.RELIEF_NONE)
554 self.network_button.show()
555 self.status_bar.pack_end(self.network_button, False, False)
558 def seedb(w, wallet):
559 if wallet.use_encryption:
560 password = password_dialog(self.window)
561 if not password: return
562 else: password = None
563 show_seed_dialog(wallet, password, self.window)
564 button = gtk.Button('S')
565 button.connect("clicked", seedb, wallet )
566 button.set_relief(gtk.RELIEF_NONE)
568 self.status_bar.pack_end(button,False, False)
570 settings_icon = gtk.Image()
571 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
572 settings_icon.set_alignment(0.5, 0.5)
573 settings_icon.set_size_request(16,16 )
576 prefs_button = gtk.Button()
577 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
578 prefs_button.add(settings_icon)
579 prefs_button.set_tooltip_text("Settings")
580 prefs_button.set_relief(gtk.RELIEF_NONE)
582 self.status_bar.pack_end(prefs_button,False,False)
584 pw_icon = gtk.Image()
585 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
586 pw_icon.set_alignment(0.5, 0.5)
587 pw_icon.set_size_request(16,16 )
591 password_button = gtk.Button()
592 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
593 password_button.add(pw_icon)
594 password_button.set_relief(gtk.RELIEF_NONE)
595 password_button.show()
596 self.status_bar.pack_end(password_button,False,False)
598 self.window.add(vbox)
599 self.window.show_all()
602 self.context_id = self.status_bar.get_context_id("statusbar")
603 self.update_status_bar()
605 self.wallet.interface.register_callback('updated', self.update_callback)
608 def update_status_bar_thread():
610 gobject.idle_add( self.update_status_bar )
614 def check_recipient_thread():
618 if self.payto_entry.is_focus():
620 r = self.payto_entry.get_text()
624 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
626 to_address = self.wallet.get_alias(r, interactive=False)
630 s = r + ' <' + to_address + '>'
631 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
634 thread.start_new_thread(update_status_bar_thread, ())
636 thread.start_new_thread(check_recipient_thread, ())
637 self.notebook.set_current_page(0)
639 def update_callback(self):
640 self.wallet_updated = True
643 def add_tab(self, page, name):
644 tab_label = gtk.Label(name)
646 self.notebook.append_page(page, tab_label)
649 def create_send_tab(self):
651 page = vbox = gtk.VBox()
655 payto_label = gtk.Label('Pay to:')
656 payto_label.set_size_request(100,-1)
657 payto.pack_start(payto_label, False)
658 payto_entry = gtk.Entry()
659 payto_entry.set_size_request(450, 26)
660 payto.pack_start(payto_entry, False)
661 vbox.pack_start(payto, False, False, 5)
664 message_label = gtk.Label('Description:')
665 message_label.set_size_request(100,-1)
666 message.pack_start(message_label, False)
667 message_entry = gtk.Entry()
668 message_entry.set_size_request(450, 26)
669 message.pack_start(message_entry, False)
670 vbox.pack_start(message, False, False, 5)
672 amount_box = gtk.HBox()
673 amount_label = gtk.Label('Amount:')
674 amount_label.set_size_request(100,-1)
675 amount_box.pack_start(amount_label, False)
676 amount_entry = gtk.Entry()
677 amount_entry.set_size_request(120, -1)
678 amount_box.pack_start(amount_entry, False)
679 vbox.pack_start(amount_box, False, False, 5)
681 self.fee_box = fee_box = gtk.HBox()
682 fee_label = gtk.Label('Fee:')
683 fee_label.set_size_request(100,-1)
684 fee_box.pack_start(fee_label, False)
685 fee_entry = gtk.Entry()
686 fee_entry.set_size_request(60, 26)
687 fee_box.pack_start(fee_entry, False)
688 vbox.pack_start(fee_box, False, False, 5)
691 empty_label = gtk.Label('')
692 empty_label.set_size_request(100,-1)
693 end_box.pack_start(empty_label, False)
694 send_button = gtk.Button("Send")
696 end_box.pack_start(send_button, False, False, 0)
697 clear_button = gtk.Button("Clear")
699 end_box.pack_start(clear_button, False, False, 15)
700 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
701 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
703 vbox.pack_start(end_box, False, False, 5)
705 # display this line only if there is a signature
706 payto_sig = gtk.HBox()
707 payto_sig_id = gtk.Label('')
708 payto_sig.pack_start(payto_sig_id, False)
709 vbox.pack_start(payto_sig, True, True, 5)
712 self.user_fee = False
714 def entry_changed( entry, is_fee ):
715 self.funds_error = False
716 amount = numbify(amount_entry)
717 fee = numbify(fee_entry)
718 if not is_fee: fee = None
721 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
723 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
726 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
727 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
728 send_button.set_sensitive(True)
730 send_button.set_sensitive(False)
731 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
732 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
733 self.funds_error = True
735 amount_entry.connect('changed', entry_changed, False)
736 fee_entry.connect('changed', entry_changed, True)
738 self.payto_entry = payto_entry
739 self.payto_fee_entry = fee_entry
740 self.payto_sig_id = payto_sig_id
741 self.payto_sig = payto_sig
742 self.amount_entry = amount_entry
743 self.message_entry = message_entry
744 self.add_tab(page, 'Send')
746 def set_frozen(self,entry,frozen):
748 entry.set_editable(False)
749 entry.set_has_frame(False)
750 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
752 entry.set_editable(True)
753 entry.set_has_frame(True)
754 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
756 def set_url(self, url):
757 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
758 self.notebook.set_current_page(1)
759 self.payto_entry.set_text(payto)
760 self.message_entry.set_text(message)
761 self.amount_entry.set_text(amount)
763 self.set_frozen(self.payto_entry,True)
764 self.set_frozen(self.amount_entry,True)
765 self.set_frozen(self.message_entry,True)
766 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
768 self.payto_sig.set_visible(False)
770 def create_about_tab(self):
775 tv.set_editable(False)
776 tv.set_cursor_visible(False)
777 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
778 scroll = gtk.ScrolledWindow()
780 page.pack_start(scroll)
781 self.info = tv.get_buffer()
782 self.add_tab(page, 'Wall')
784 def do_clear(self, w, data):
785 self.payto_sig.set_visible(False)
786 self.payto_fee_entry.set_text('')
787 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
788 self.set_frozen(entry,False)
791 def question(self,msg):
792 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
794 result = dialog.run()
796 return result == gtk.RESPONSE_OK
798 def do_send(self, w, data):
799 payto_entry, label_entry, amount_entry, fee_entry = data
800 label = label_entry.get_text()
801 r = payto_entry.get_text()
804 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
805 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
808 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
812 self.update_sending_tab()
815 to_address = m2.group(5)
819 if not is_valid(to_address):
820 self.show_message( "invalid bitcoin address:\n"+to_address)
824 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
826 self.show_message( "invalid amount")
829 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
831 self.show_message( "invalid fee")
834 if self.wallet.use_encryption:
835 password = password_dialog(self.window)
842 tx = self.wallet.mktx( [(to_address, amount)], password, fee )
843 except BaseException, e:
844 self.show_message(str(e))
848 self.wallet.labels[tx.hash()] = label
850 status, msg = self.wallet.sendtx( tx )
852 self.show_message( "payment sent.\n" + msg )
853 payto_entry.set_text("")
854 label_entry.set_text("")
855 amount_entry.set_text("")
856 fee_entry.set_text("")
858 self.update_sending_tab()
860 self.show_message( msg )
863 def treeview_button_press(self, treeview, event):
864 if event.type == gtk.gdk._2BUTTON_PRESS:
865 c = treeview.get_cursor()[0]
866 if treeview == self.history_treeview:
867 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
868 self.show_message(tx_details)
869 elif treeview == self.contacts_treeview:
870 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
871 #a = self.wallet.aliases.get(m)
873 # if a[0] in self.wallet.authorities.keys():
874 # s = self.wallet.authorities.get(a[0])
877 # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
878 # self.show_message(msg)
881 def treeview_key_press(self, treeview, event):
882 c = treeview.get_cursor()[0]
883 if event.keyval == gtk.keysyms.Up:
885 treeview.parent.grab_focus()
886 treeview.set_cursor((0,))
887 elif event.keyval == gtk.keysyms.Return:
888 if treeview == self.history_treeview:
889 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
890 self.show_message(tx_details)
891 elif treeview == self.contacts_treeview:
892 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
893 #a = self.wallet.aliases.get(m)
895 # if a[0] in self.wallet.authorities.keys():
896 # s = self.wallet.authorities.get(a[0])
899 # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
900 # self.show_message(msg)
904 def create_history_tab(self):
906 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
907 treeview = gtk.TreeView(model=self.history_list)
908 self.history_treeview = treeview
909 treeview.set_tooltip_column(7)
911 treeview.connect('key-press-event', self.treeview_key_press)
912 treeview.connect('button-press-event', self.treeview_button_press)
914 tvcolumn = gtk.TreeViewColumn('')
915 treeview.append_column(tvcolumn)
916 cell = gtk.CellRendererPixbuf()
917 tvcolumn.pack_start(cell, False)
918 tvcolumn.set_attributes(cell, stock_id=1)
920 tvcolumn = gtk.TreeViewColumn('Date')
921 treeview.append_column(tvcolumn)
922 cell = gtk.CellRendererText()
923 tvcolumn.pack_start(cell, False)
924 tvcolumn.add_attribute(cell, 'text', 2)
926 tvcolumn = gtk.TreeViewColumn('Description')
927 treeview.append_column(tvcolumn)
928 cell = gtk.CellRendererText()
929 cell.set_property('foreground', 'grey')
930 cell.set_property('family', MONOSPACE_FONT)
931 cell.set_property('editable', True)
932 def edited_cb(cell, path, new_text, h_list):
933 tx = h_list.get_value( h_list.get_iter(path), 0)
934 self.wallet.labels[tx] = new_text
936 self.update_history_tab()
937 cell.connect('edited', edited_cb, self.history_list)
938 def editing_started(cell, entry, path, h_list):
939 tx = h_list.get_value( h_list.get_iter(path), 0)
940 if not self.wallet.labels.get(tx): entry.set_text('')
941 cell.connect('editing-started', editing_started, self.history_list)
942 tvcolumn.set_expand(True)
943 tvcolumn.pack_start(cell, True)
944 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
946 tvcolumn = gtk.TreeViewColumn('Amount')
947 treeview.append_column(tvcolumn)
948 cell = gtk.CellRendererText()
949 cell.set_alignment(1, 0.5)
950 cell.set_property('family', MONOSPACE_FONT)
951 tvcolumn.pack_start(cell, False)
952 tvcolumn.add_attribute(cell, 'text', 5)
954 tvcolumn = gtk.TreeViewColumn('Balance')
955 treeview.append_column(tvcolumn)
956 cell = gtk.CellRendererText()
957 cell.set_alignment(1, 0.5)
958 cell.set_property('family', MONOSPACE_FONT)
959 tvcolumn.pack_start(cell, False)
960 tvcolumn.add_attribute(cell, 'text', 6)
962 tvcolumn = gtk.TreeViewColumn('Tooltip')
963 treeview.append_column(tvcolumn)
964 cell = gtk.CellRendererText()
965 tvcolumn.pack_start(cell, False)
966 tvcolumn.add_attribute(cell, 'text', 7)
967 tvcolumn.set_visible(False)
969 scroll = gtk.ScrolledWindow()
970 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
973 self.add_tab(scroll, 'History')
974 self.update_history_tab()
977 def create_recv_tab(self):
978 self.recv_list = gtk.ListStore(str, str, str)
979 self.add_tab( self.make_address_list(True), 'Receive')
980 self.update_receiving_tab()
982 def create_book_tab(self):
983 self.addressbook_list = gtk.ListStore(str, str, str)
984 self.add_tab( self.make_address_list(False), 'Contacts')
985 self.update_sending_tab()
987 def make_address_list(self, is_recv):
988 liststore = self.recv_list if is_recv else self.addressbook_list
989 treeview = gtk.TreeView(model= liststore)
990 treeview.connect('key-press-event', self.treeview_key_press)
991 treeview.connect('button-press-event', self.treeview_button_press)
994 self.contacts_treeview = treeview
996 tvcolumn = gtk.TreeViewColumn('Address')
997 treeview.append_column(tvcolumn)
998 cell = gtk.CellRendererText()
999 cell.set_property('family', MONOSPACE_FONT)
1000 tvcolumn.pack_start(cell, True)
1001 tvcolumn.add_attribute(cell, 'text', 0)
1003 tvcolumn = gtk.TreeViewColumn('Label')
1004 tvcolumn.set_expand(True)
1005 treeview.append_column(tvcolumn)
1006 cell = gtk.CellRendererText()
1007 cell.set_property('editable', True)
1008 def edited_cb2(cell, path, new_text, liststore):
1009 address = liststore.get_value( liststore.get_iter(path), 0)
1010 self.wallet.labels[address] = new_text
1012 self.update_receiving_tab()
1013 self.update_sending_tab()
1014 self.update_history_tab()
1015 cell.connect('edited', edited_cb2, liststore)
1016 tvcolumn.pack_start(cell, True)
1017 tvcolumn.add_attribute(cell, 'text', 1)
1019 tvcolumn = gtk.TreeViewColumn('Tx')
1020 treeview.append_column(tvcolumn)
1021 cell = gtk.CellRendererText()
1022 tvcolumn.pack_start(cell, True)
1023 tvcolumn.add_attribute(cell, 'text', 2)
1025 scroll = gtk.ScrolledWindow()
1026 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1027 scroll.add(treeview)
1031 button = gtk.Button("New")
1032 button.connect("clicked", self.newaddress_dialog)
1034 hbox.pack_start(button,False)
1036 def showqrcode(w, treeview, liststore):
1037 path, col = treeview.get_cursor()
1039 address = liststore.get_value(liststore.get_iter(path), 0)
1040 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1044 size = qr.getModuleCount()*boxsize
1045 def area_expose_cb(area, event):
1046 style = area.get_style()
1047 k = qr.getModuleCount()
1050 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1051 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1052 area = gtk.DrawingArea()
1053 area.set_size_request(size, size)
1054 area.connect("expose-event", area_expose_cb)
1056 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1057 dialog.vbox.add(area)
1061 button = gtk.Button("QR")
1062 button.connect("clicked", showqrcode, treeview, liststore)
1064 hbox.pack_start(button,False)
1066 button = gtk.Button("Copy to clipboard")
1067 def copy2clipboard(w, treeview, liststore):
1069 path, col = treeview.get_cursor()
1071 address = liststore.get_value( liststore.get_iter(path), 0)
1072 if platform.system() == 'Windows':
1073 from Tkinter import Tk
1077 r.clipboard_append( address )
1080 c = gtk.clipboard_get()
1081 c.set_text( address )
1082 button.connect("clicked", copy2clipboard, treeview, liststore)
1084 hbox.pack_start(button,False)
1087 button = gtk.Button("Pay to")
1088 def payto(w, treeview, liststore):
1089 path, col = treeview.get_cursor()
1091 address = liststore.get_value( liststore.get_iter(path), 0)
1092 self.payto_entry.set_text( address )
1093 self.notebook.set_current_page(1)
1094 self.amount_entry.grab_focus()
1096 button.connect("clicked", payto, treeview, liststore)
1098 hbox.pack_start(button,False)
1101 vbox.pack_start(scroll,True)
1102 vbox.pack_start(hbox, False)
1105 def update_status_bar(self):
1106 interface = self.wallet.interface
1107 if self.funds_error:
1108 text = "Not enough funds"
1109 elif interface and interface.is_connected:
1110 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1111 if not self.wallet.up_to_date:
1112 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1113 text = "Synchronizing..."
1115 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1116 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1117 c, u = self.wallet.get_balance()
1118 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1119 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1121 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1122 self.network_button.set_tooltip_text("Not connected.")
1123 text = "Not connected"
1125 self.status_bar.pop(self.context_id)
1126 self.status_bar.push(self.context_id, text)
1128 if self.wallet.up_to_date and self.wallet_updated:
1129 self.update_history_tab()
1130 self.update_receiving_tab()
1131 # addressbook too...
1132 self.info.set_text( self.wallet.banner )
1133 self.wallet_updated = False
1135 def update_receiving_tab(self):
1136 self.recv_list.clear()
1137 for address in self.wallet.addresses(True):
1138 if self.wallet.is_change(address):continue
1139 label = self.wallet.labels.get(address)
1140 h = self.wallet.history.get(address,[])
1142 tx = "None" if n==0 else "%d"%n
1143 self.recv_list.append((address, label, tx ))
1145 def update_sending_tab(self):
1146 # detect addresses that are not mine in history, add them here...
1147 self.addressbook_list.clear()
1148 #for alias, v in self.wallet.aliases.items():
1150 # label = self.wallet.labels.get(alias)
1151 # self.addressbook_list.append((alias, label, '-'))
1153 for address in self.wallet.addressbook:
1154 label = self.wallet.labels.get(address)
1156 for tx in self.wallet.transactions.values():
1157 if address in map(lambda x:x[0], tx.outputs): n += 1
1159 self.addressbook_list.append((address, label, "%d"%n))
1161 def update_history_tab(self):
1162 cursor = self.history_treeview.get_cursor()[0]
1163 self.history_list.clear()
1165 for item in self.wallet.get_tx_history():
1166 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1169 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1172 conf_icon = gtk.STOCK_APPLY
1174 time_str = 'pending'
1175 conf_icon = gtk.STOCK_EXECUTE
1177 label, is_default_label = self.wallet.get_label(tx_hash)
1178 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1179 details = self.get_tx_details(tx_hash)
1181 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1182 format_satoshis(value,True,self.wallet.num_zeros),
1183 format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1184 if cursor: self.history_treeview.set_cursor( cursor )
1187 def get_tx_details(self, tx_hash):
1189 if not tx_hash: return ''
1190 tx = self.wallet.transactions.get(tx_hash)
1191 is_mine, v, fee = self.wallet.get_tx_value(tx)
1192 conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1195 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1197 time_str = 'pending'
1199 inputs = map(lambda x: x.get('address'), tx.inputs)
1200 outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1201 tx_details = "Transaction Details" +"\n\n" \
1202 + "Transaction ID:\n" + tx_hash + "\n\n" \
1203 + "Status: %d confirmations\n"%conf
1206 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1207 + "Transaction fee: %s\n"% format_satoshis(fee, False)
1209 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1210 + "Transaction fee: unknown\n"
1212 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1214 tx_details += "Date: %s\n\n"%time_str \
1215 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1216 + "Outputs:\n-"+ '\n-'.join(outputs)
1222 def newaddress_dialog(self, w):
1224 title = "New Contact"
1225 dialog = gtk.Dialog(title, parent=self.window,
1226 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1227 buttons= ("cancel", 0, "ok",1) )
1231 label_label = gtk.Label('Label:')
1232 label_label.set_size_request(120,10)
1234 label.pack_start(label_label)
1235 label_entry = gtk.Entry()
1237 label.pack_start(label_entry)
1239 dialog.vbox.pack_start(label, False, True, 5)
1241 address = gtk.HBox()
1242 address_label = gtk.Label('Address:')
1243 address_label.set_size_request(120,10)
1244 address_label.show()
1245 address.pack_start(address_label)
1246 address_entry = gtk.Entry()
1247 address_entry.show()
1248 address.pack_start(address_entry)
1250 dialog.vbox.pack_start(address, False, True, 5)
1252 result = dialog.run()
1253 address = address_entry.get_text()
1254 label = label_entry.get_text()
1258 if is_valid(address):
1259 self.wallet.addressbook.append(address)
1260 if label: self.wallet.labels[address] = label
1262 self.update_sending_tab()
1264 errorDialog = gtk.MessageDialog(
1266 flags=gtk.DIALOG_MODAL,
1267 buttons= gtk.BUTTONS_CLOSE,
1268 message_format = "Invalid address")
1271 errorDialog.destroy()
1275 class ElectrumGui():
1277 def __init__(self, wallet, config):
1278 self.wallet = wallet
1279 self.config = config
1281 def main(self, url=None):
1282 ew = ElectrumWindow(self.wallet, self.config)
1283 if url: ew.set_url(url)
1286 def restore_or_create(self):
1287 return restore_create_dialog(self.wallet)
1289 def seed_dialog(self):
1290 return run_recovery_dialog( self.wallet )
1292 def network_dialog(self):
1293 return run_network_dialog( self.wallet, parent=None )
1295 def show_seed(self):
1296 show_seed_dialog(self.wallet, None, None)
1298 def password_dialog(self):
1299 change_password_dialog(self.wallet, None, None)
1301 def restore_wallet(self):
1302 wallet = self.wallet
1304 dialog = gtk.MessageDialog(
1306 flags = gtk.DIALOG_MODAL,
1307 buttons = gtk.BUTTONS_CANCEL,
1308 message_format = "Please wait..." )
1311 def recover_thread( wallet, dialog ):
1312 while not wallet.is_up_to_date():
1314 gobject.idle_add( dialog.destroy )
1316 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1319 if r==gtk.RESPONSE_CANCEL: return False
1320 if not wallet.is_found():
1321 show_message("No transactions found for this seed")