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 kilobyte of transaction. Recommended value:0.0001')
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))
847 if tx.requires_fee(self.wallet.verifier) and fee == 0:
848 self.show_message( "This transaction requires a fee, or it will not be propagated by the network." )
853 self.wallet.labels[tx.hash()] = label
855 status, msg = self.wallet.sendtx( tx )
857 self.show_message( "payment sent.\n" + msg )
858 payto_entry.set_text("")
859 label_entry.set_text("")
860 amount_entry.set_text("")
861 fee_entry.set_text("")
863 self.update_sending_tab()
865 self.show_message( msg )
868 def treeview_button_press(self, treeview, event):
869 if event.type == gtk.gdk._2BUTTON_PRESS:
870 c = treeview.get_cursor()[0]
871 if treeview == self.history_treeview:
872 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
873 self.show_message(tx_details)
874 elif treeview == self.contacts_treeview:
875 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
876 #a = self.wallet.aliases.get(m)
878 # if a[0] in self.wallet.authorities.keys():
879 # s = self.wallet.authorities.get(a[0])
882 # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
883 # self.show_message(msg)
886 def treeview_key_press(self, treeview, event):
887 c = treeview.get_cursor()[0]
888 if event.keyval == gtk.keysyms.Up:
890 treeview.parent.grab_focus()
891 treeview.set_cursor((0,))
892 elif event.keyval == gtk.keysyms.Return:
893 if treeview == self.history_treeview:
894 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
895 self.show_message(tx_details)
896 elif treeview == self.contacts_treeview:
897 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
898 #a = self.wallet.aliases.get(m)
900 # if a[0] in self.wallet.authorities.keys():
901 # s = self.wallet.authorities.get(a[0])
904 # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
905 # self.show_message(msg)
909 def create_history_tab(self):
911 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
912 treeview = gtk.TreeView(model=self.history_list)
913 self.history_treeview = treeview
914 treeview.set_tooltip_column(7)
916 treeview.connect('key-press-event', self.treeview_key_press)
917 treeview.connect('button-press-event', self.treeview_button_press)
919 tvcolumn = gtk.TreeViewColumn('')
920 treeview.append_column(tvcolumn)
921 cell = gtk.CellRendererPixbuf()
922 tvcolumn.pack_start(cell, False)
923 tvcolumn.set_attributes(cell, stock_id=1)
925 tvcolumn = gtk.TreeViewColumn('Date')
926 treeview.append_column(tvcolumn)
927 cell = gtk.CellRendererText()
928 tvcolumn.pack_start(cell, False)
929 tvcolumn.add_attribute(cell, 'text', 2)
931 tvcolumn = gtk.TreeViewColumn('Description')
932 treeview.append_column(tvcolumn)
933 cell = gtk.CellRendererText()
934 cell.set_property('foreground', 'grey')
935 cell.set_property('family', MONOSPACE_FONT)
936 cell.set_property('editable', True)
937 def edited_cb(cell, path, new_text, h_list):
938 tx = h_list.get_value( h_list.get_iter(path), 0)
939 self.wallet.labels[tx] = new_text
941 self.update_history_tab()
942 cell.connect('edited', edited_cb, self.history_list)
943 def editing_started(cell, entry, path, h_list):
944 tx = h_list.get_value( h_list.get_iter(path), 0)
945 if not self.wallet.labels.get(tx): entry.set_text('')
946 cell.connect('editing-started', editing_started, self.history_list)
947 tvcolumn.set_expand(True)
948 tvcolumn.pack_start(cell, True)
949 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
951 tvcolumn = gtk.TreeViewColumn('Amount')
952 treeview.append_column(tvcolumn)
953 cell = gtk.CellRendererText()
954 cell.set_alignment(1, 0.5)
955 cell.set_property('family', MONOSPACE_FONT)
956 tvcolumn.pack_start(cell, False)
957 tvcolumn.add_attribute(cell, 'text', 5)
959 tvcolumn = gtk.TreeViewColumn('Balance')
960 treeview.append_column(tvcolumn)
961 cell = gtk.CellRendererText()
962 cell.set_alignment(1, 0.5)
963 cell.set_property('family', MONOSPACE_FONT)
964 tvcolumn.pack_start(cell, False)
965 tvcolumn.add_attribute(cell, 'text', 6)
967 tvcolumn = gtk.TreeViewColumn('Tooltip')
968 treeview.append_column(tvcolumn)
969 cell = gtk.CellRendererText()
970 tvcolumn.pack_start(cell, False)
971 tvcolumn.add_attribute(cell, 'text', 7)
972 tvcolumn.set_visible(False)
974 scroll = gtk.ScrolledWindow()
975 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
978 self.add_tab(scroll, 'History')
979 self.update_history_tab()
982 def create_recv_tab(self):
983 self.recv_list = gtk.ListStore(str, str, str)
984 self.add_tab( self.make_address_list(True), 'Receive')
985 self.update_receiving_tab()
987 def create_book_tab(self):
988 self.addressbook_list = gtk.ListStore(str, str, str)
989 self.add_tab( self.make_address_list(False), 'Contacts')
990 self.update_sending_tab()
992 def make_address_list(self, is_recv):
993 liststore = self.recv_list if is_recv else self.addressbook_list
994 treeview = gtk.TreeView(model= liststore)
995 treeview.connect('key-press-event', self.treeview_key_press)
996 treeview.connect('button-press-event', self.treeview_button_press)
999 self.contacts_treeview = treeview
1001 tvcolumn = gtk.TreeViewColumn('Address')
1002 treeview.append_column(tvcolumn)
1003 cell = gtk.CellRendererText()
1004 cell.set_property('family', MONOSPACE_FONT)
1005 tvcolumn.pack_start(cell, True)
1006 tvcolumn.add_attribute(cell, 'text', 0)
1008 tvcolumn = gtk.TreeViewColumn('Label')
1009 tvcolumn.set_expand(True)
1010 treeview.append_column(tvcolumn)
1011 cell = gtk.CellRendererText()
1012 cell.set_property('editable', True)
1013 def edited_cb2(cell, path, new_text, liststore):
1014 address = liststore.get_value( liststore.get_iter(path), 0)
1015 self.wallet.labels[address] = new_text
1017 self.update_receiving_tab()
1018 self.update_sending_tab()
1019 self.update_history_tab()
1020 cell.connect('edited', edited_cb2, liststore)
1021 tvcolumn.pack_start(cell, True)
1022 tvcolumn.add_attribute(cell, 'text', 1)
1024 tvcolumn = gtk.TreeViewColumn('Tx')
1025 treeview.append_column(tvcolumn)
1026 cell = gtk.CellRendererText()
1027 tvcolumn.pack_start(cell, True)
1028 tvcolumn.add_attribute(cell, 'text', 2)
1030 scroll = gtk.ScrolledWindow()
1031 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1032 scroll.add(treeview)
1036 button = gtk.Button("New")
1037 button.connect("clicked", self.newaddress_dialog)
1039 hbox.pack_start(button,False)
1041 def showqrcode(w, treeview, liststore):
1042 path, col = treeview.get_cursor()
1044 address = liststore.get_value(liststore.get_iter(path), 0)
1045 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1049 size = qr.getModuleCount()*boxsize
1050 def area_expose_cb(area, event):
1051 style = area.get_style()
1052 k = qr.getModuleCount()
1055 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1056 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1057 area = gtk.DrawingArea()
1058 area.set_size_request(size, size)
1059 area.connect("expose-event", area_expose_cb)
1061 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1062 dialog.vbox.add(area)
1066 button = gtk.Button("QR")
1067 button.connect("clicked", showqrcode, treeview, liststore)
1069 hbox.pack_start(button,False)
1071 button = gtk.Button("Copy to clipboard")
1072 def copy2clipboard(w, treeview, liststore):
1074 path, col = treeview.get_cursor()
1076 address = liststore.get_value( liststore.get_iter(path), 0)
1077 if platform.system() == 'Windows':
1078 from Tkinter import Tk
1082 r.clipboard_append( address )
1085 c = gtk.clipboard_get()
1086 c.set_text( address )
1087 button.connect("clicked", copy2clipboard, treeview, liststore)
1089 hbox.pack_start(button,False)
1092 button = gtk.Button("Pay to")
1093 def payto(w, treeview, liststore):
1094 path, col = treeview.get_cursor()
1096 address = liststore.get_value( liststore.get_iter(path), 0)
1097 self.payto_entry.set_text( address )
1098 self.notebook.set_current_page(1)
1099 self.amount_entry.grab_focus()
1101 button.connect("clicked", payto, treeview, liststore)
1103 hbox.pack_start(button,False)
1106 vbox.pack_start(scroll,True)
1107 vbox.pack_start(hbox, False)
1110 def update_status_bar(self):
1111 interface = self.wallet.interface
1112 if self.funds_error:
1113 text = "Not enough funds"
1114 elif interface and interface.is_connected:
1115 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1116 if not self.wallet.up_to_date:
1117 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1118 text = "Synchronizing..."
1120 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1121 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1122 c, u = self.wallet.get_balance()
1123 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1124 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1126 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1127 self.network_button.set_tooltip_text("Not connected.")
1128 text = "Not connected"
1130 self.status_bar.pop(self.context_id)
1131 self.status_bar.push(self.context_id, text)
1133 if self.wallet.up_to_date and self.wallet_updated:
1134 self.update_history_tab()
1135 self.update_receiving_tab()
1136 # addressbook too...
1137 self.info.set_text( self.wallet.interface.banner )
1138 self.wallet_updated = False
1140 def update_receiving_tab(self):
1141 self.recv_list.clear()
1142 for address in self.wallet.addresses(True):
1143 if self.wallet.is_change(address):continue
1144 label = self.wallet.labels.get(address)
1145 h = self.wallet.history.get(address,[])
1147 tx = "None" if n==0 else "%d"%n
1148 self.recv_list.append((address, label, tx ))
1150 def update_sending_tab(self):
1151 # detect addresses that are not mine in history, add them here...
1152 self.addressbook_list.clear()
1153 #for alias, v in self.wallet.aliases.items():
1155 # label = self.wallet.labels.get(alias)
1156 # self.addressbook_list.append((alias, label, '-'))
1158 for address in self.wallet.addressbook:
1159 label = self.wallet.labels.get(address)
1160 n = self.wallet.get_num_tx(address)
1161 self.addressbook_list.append((address, label, "%d"%n))
1163 def update_history_tab(self):
1164 cursor = self.history_treeview.get_cursor()[0]
1165 self.history_list.clear()
1167 for item in self.wallet.get_tx_history():
1168 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1171 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1174 conf_icon = gtk.STOCK_APPLY
1176 time_str = 'pending'
1177 conf_icon = gtk.STOCK_EXECUTE
1179 label, is_default_label = self.wallet.get_label(tx_hash)
1180 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1181 details = self.get_tx_details(tx_hash)
1183 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1184 format_satoshis(value,True,self.wallet.num_zeros),
1185 format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1186 if cursor: self.history_treeview.set_cursor( cursor )
1189 def get_tx_details(self, tx_hash):
1191 if not tx_hash: return ''
1192 tx = self.wallet.transactions.get(tx_hash)
1193 is_mine, v, fee = self.wallet.get_tx_value(tx)
1194 conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1197 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1199 time_str = 'pending'
1201 inputs = map(lambda x: x.get('address'), tx.inputs)
1202 outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1203 tx_details = "Transaction Details" +"\n\n" \
1204 + "Transaction ID:\n" + tx_hash + "\n\n" \
1205 + "Status: %d confirmations\n"%conf
1208 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1209 + "Transaction fee: %s\n"% format_satoshis(fee, False)
1211 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1212 + "Transaction fee: unknown\n"
1214 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1216 tx_details += "Date: %s\n\n"%time_str \
1217 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1218 + "Outputs:\n-"+ '\n-'.join(outputs)
1224 def newaddress_dialog(self, w):
1226 title = "New Contact"
1227 dialog = gtk.Dialog(title, parent=self.window,
1228 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1229 buttons= ("cancel", 0, "ok",1) )
1233 label_label = gtk.Label('Label:')
1234 label_label.set_size_request(120,10)
1236 label.pack_start(label_label)
1237 label_entry = gtk.Entry()
1239 label.pack_start(label_entry)
1241 dialog.vbox.pack_start(label, False, True, 5)
1243 address = gtk.HBox()
1244 address_label = gtk.Label('Address:')
1245 address_label.set_size_request(120,10)
1246 address_label.show()
1247 address.pack_start(address_label)
1248 address_entry = gtk.Entry()
1249 address_entry.show()
1250 address.pack_start(address_entry)
1252 dialog.vbox.pack_start(address, False, True, 5)
1254 result = dialog.run()
1255 address = address_entry.get_text()
1256 label = label_entry.get_text()
1260 if is_valid(address):
1261 self.wallet.addressbook.append(address)
1262 if label: self.wallet.labels[address] = label
1264 self.update_sending_tab()
1266 errorDialog = gtk.MessageDialog(
1268 flags=gtk.DIALOG_MODAL,
1269 buttons= gtk.BUTTONS_CLOSE,
1270 message_format = "Invalid address")
1273 errorDialog.destroy()
1277 class ElectrumGui():
1279 def __init__(self, wallet, config):
1280 self.wallet = wallet
1281 self.config = config
1283 def main(self, url=None):
1284 ew = ElectrumWindow(self.wallet, self.config)
1285 if url: ew.set_url(url)
1288 def restore_or_create(self):
1289 return restore_create_dialog(self.wallet)
1291 def seed_dialog(self):
1292 return run_recovery_dialog( self.wallet )
1294 def network_dialog(self):
1295 return run_network_dialog( self.wallet, parent=None )
1297 def show_seed(self):
1298 show_seed_dialog(self.wallet, None, None)
1300 def password_dialog(self):
1301 change_password_dialog(self.wallet, None, None)
1303 def restore_wallet(self):
1304 wallet = self.wallet
1306 dialog = gtk.MessageDialog(
1308 flags = gtk.DIALOG_MODAL,
1309 buttons = gtk.BUTTONS_CANCEL,
1310 message_format = "Please wait..." )
1313 def recover_thread( wallet, dialog ):
1314 while not wallet.is_up_to_date():
1316 gobject.idle_add( dialog.destroy )
1318 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1321 if r==gtk.RESPONSE_CANCEL: return False
1322 if not wallet.is_found():
1323 show_message("No transactions found for this seed")