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")
252 if wallet.fee != fee:
260 show_message("error")
262 if wallet.num_zeros != nz:
263 wallet.num_zeros = nz
266 wallet.config.set_key('gui',gui,True)
271 def run_network_dialog( wallet, parent ):
273 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
274 interface = wallet.interface
276 if interface.is_connected:
277 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.verifier.height)
279 status = "Not connected"
282 status = "Please choose a server.\nSelect cancel if you are offline."
284 server = interface.server
285 servers = interface.get_servers()
287 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
288 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
289 dialog.set_title("Server")
290 dialog.set_image(image)
294 host_box = gtk.HBox()
295 host_label = gtk.Label('Connect to:')
296 host_label.set_size_request(100,-1)
298 host_box.pack_start(host_label, False, False, 10)
299 host_entry = gtk.Entry()
300 host_entry.set_size_request(200,-1)
301 host_entry.set_text(server)
303 host_box.pack_start(host_entry, False, False, 10)
304 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)')
308 p_box = gtk.HBox(False, 10)
311 p_label = gtk.Label('Protocol:')
312 p_label.set_size_request(100,-1)
314 p_box.pack_start(p_label, False, False, 10)
316 radio1 = gtk.RadioButton(None, "tcp")
317 p_box.pack_start(radio1, True, True, 0)
319 radio2 = gtk.RadioButton(radio1, "http")
320 p_box.pack_start(radio2, True, True, 0)
324 return unicode(host_entry.get_text()).split(':')
326 def set_button(protocol):
329 elif protocol == 'h':
332 def set_protocol(protocol):
333 host = current_line()[0]
335 if protocol not in pp.keys():
336 protocol = pp.keys()[0]
339 host_entry.set_text( host + ':' + port + ':' + protocol)
341 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
342 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
344 server_list = gtk.ListStore(str)
345 for host in servers.keys():
346 server_list.append([host])
348 treeview = gtk.TreeView(model=server_list)
351 if wallet.interface.servers:
352 label = 'Active Servers'
354 label = 'Default Servers'
356 tvcolumn = gtk.TreeViewColumn(label)
357 treeview.append_column(tvcolumn)
358 cell = gtk.CellRendererText()
359 tvcolumn.pack_start(cell, False)
360 tvcolumn.add_attribute(cell, 'text', 0)
362 scroll = gtk.ScrolledWindow()
363 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
367 vbox.pack_start(host_box, False,False, 5)
368 vbox.pack_start(p_box, True, True, 0)
369 vbox.pack_start(scroll)
371 def my_treeview_cb(treeview):
372 path, view_column = treeview.get_cursor()
373 host = server_list.get_value( server_list.get_iter(path), 0)
379 protocol = pp.keys()[0]
381 host_entry.set_text( host + ':' + port + ':' + protocol)
384 treeview.connect('cursor-changed', my_treeview_cb)
388 server = host_entry.get_text()
391 if r==gtk.RESPONSE_CANCEL:
395 interface.set_server(server)
397 show_message("error:" + server)
401 wallet.config.set_key("server", server, True)
406 def show_message(message, parent=None):
407 dialog = gtk.MessageDialog(
409 flags = gtk.DIALOG_MODAL,
410 buttons = gtk.BUTTONS_CLOSE,
411 message_format = message )
416 def password_line(label):
417 password = gtk.HBox()
418 password_label = gtk.Label(label)
419 password_label.set_size_request(120,10)
420 password_label.show()
421 password.pack_start(password_label,False, False, 10)
422 password_entry = gtk.Entry()
423 password_entry.set_size_request(300,-1)
424 password_entry.set_visibility(False)
425 password_entry.show()
426 password.pack_start(password_entry,False,False, 10)
428 return password, password_entry
430 def password_dialog(parent):
431 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
432 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
433 dialog.get_image().set_visible(False)
434 current_pw, current_pw_entry = password_line('Password:')
435 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
436 dialog.vbox.pack_start(current_pw, False, True, 0)
438 result = dialog.run()
439 pw = current_pw_entry.get_text()
441 if result != gtk.RESPONSE_CANCEL: return pw
443 def change_password_dialog(wallet, parent, icon):
445 show_message("No seed")
449 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'
451 msg = "Please choose a password to encrypt your wallet keys"
453 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
454 dialog.set_title("Change password")
456 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
458 dialog.set_image(image)
460 if wallet.use_encryption:
461 current_pw, current_pw_entry = password_line('Current password:')
462 dialog.vbox.pack_start(current_pw, False, True, 0)
464 password, password_entry = password_line('New password:')
465 dialog.vbox.pack_start(password, False, True, 5)
466 password2, password2_entry = password_line('Confirm password:')
467 dialog.vbox.pack_start(password2, False, True, 5)
470 result = dialog.run()
471 password = current_pw_entry.get_text() if wallet.use_encryption else None
472 new_password = password_entry.get_text()
473 new_password2 = password2_entry.get_text()
475 if result == gtk.RESPONSE_CANCEL:
479 seed = wallet.decode_seed(password)
481 show_message("Incorrect password")
484 if new_password != new_password2:
485 show_message("passwords do not match")
488 wallet.update_password(seed, password, new_password)
491 if wallet.use_encryption:
492 icon.set_tooltip_text('wallet is encrypted')
494 icon.set_tooltip_text('wallet is unencrypted')
497 def add_help_button(hbox, message):
498 button = gtk.Button('?')
499 button.connect("clicked", lambda x: show_message(message))
501 hbox.pack_start(button,False, False)
504 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
506 gobject.type_register(MyWindow)
507 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
508 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
511 class ElectrumWindow:
513 def show_message(self, msg):
514 show_message(msg, self.window)
516 def __init__(self, wallet, config):
519 self.funds_error = False # True if not enough funds
521 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
522 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
523 if not self.wallet.seed: title += ' [seedless]'
524 self.window.set_title(title)
525 self.window.connect("destroy", gtk.main_quit)
526 self.window.set_border_width(0)
527 self.window.connect('mykeypress', gtk.main_quit)
528 self.window.set_default_size(720, 350)
529 self.wallet_updated = False
533 self.notebook = gtk.Notebook()
534 self.create_history_tab()
536 self.create_send_tab()
537 self.create_recv_tab()
538 self.create_book_tab()
539 self.create_about_tab()
541 vbox.pack_start(self.notebook, True, True, 2)
543 self.status_bar = gtk.Statusbar()
544 vbox.pack_start(self.status_bar, False, False, 0)
546 self.status_image = gtk.Image()
547 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
548 self.status_image.set_alignment(True, 0.5 )
549 self.status_image.show()
551 self.network_button = gtk.Button()
552 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
553 self.network_button.add(self.status_image)
554 self.network_button.set_relief(gtk.RELIEF_NONE)
555 self.network_button.show()
556 self.status_bar.pack_end(self.network_button, False, False)
559 def seedb(w, wallet):
560 if wallet.use_encryption:
561 password = password_dialog(self.window)
562 if not password: return
563 else: password = None
564 show_seed_dialog(wallet, password, self.window)
565 button = gtk.Button('S')
566 button.connect("clicked", seedb, wallet )
567 button.set_relief(gtk.RELIEF_NONE)
569 self.status_bar.pack_end(button,False, False)
571 settings_icon = gtk.Image()
572 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
573 settings_icon.set_alignment(0.5, 0.5)
574 settings_icon.set_size_request(16,16 )
577 prefs_button = gtk.Button()
578 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
579 prefs_button.add(settings_icon)
580 prefs_button.set_tooltip_text("Settings")
581 prefs_button.set_relief(gtk.RELIEF_NONE)
583 self.status_bar.pack_end(prefs_button,False,False)
585 pw_icon = gtk.Image()
586 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
587 pw_icon.set_alignment(0.5, 0.5)
588 pw_icon.set_size_request(16,16 )
592 password_button = gtk.Button()
593 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
594 password_button.add(pw_icon)
595 password_button.set_relief(gtk.RELIEF_NONE)
596 password_button.show()
597 self.status_bar.pack_end(password_button,False,False)
599 self.window.add(vbox)
600 self.window.show_all()
603 self.context_id = self.status_bar.get_context_id("statusbar")
604 self.update_status_bar()
606 self.wallet.interface.register_callback('updated', self.update_callback)
609 def update_status_bar_thread():
611 gobject.idle_add( self.update_status_bar )
615 def check_recipient_thread():
619 if self.payto_entry.is_focus():
621 r = self.payto_entry.get_text()
625 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
627 to_address = self.wallet.get_alias(r, interactive=False)
631 s = r + ' <' + to_address + '>'
632 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
635 thread.start_new_thread(update_status_bar_thread, ())
637 thread.start_new_thread(check_recipient_thread, ())
638 self.notebook.set_current_page(0)
640 def update_callback(self):
641 self.wallet_updated = True
644 def add_tab(self, page, name):
645 tab_label = gtk.Label(name)
647 self.notebook.append_page(page, tab_label)
650 def create_send_tab(self):
652 page = vbox = gtk.VBox()
656 payto_label = gtk.Label('Pay to:')
657 payto_label.set_size_request(100,-1)
658 payto.pack_start(payto_label, False)
659 payto_entry = gtk.Entry()
660 payto_entry.set_size_request(450, 26)
661 payto.pack_start(payto_entry, False)
662 vbox.pack_start(payto, False, False, 5)
665 message_label = gtk.Label('Description:')
666 message_label.set_size_request(100,-1)
667 message.pack_start(message_label, False)
668 message_entry = gtk.Entry()
669 message_entry.set_size_request(450, 26)
670 message.pack_start(message_entry, False)
671 vbox.pack_start(message, False, False, 5)
673 amount_box = gtk.HBox()
674 amount_label = gtk.Label('Amount:')
675 amount_label.set_size_request(100,-1)
676 amount_box.pack_start(amount_label, False)
677 amount_entry = gtk.Entry()
678 amount_entry.set_size_request(120, -1)
679 amount_box.pack_start(amount_entry, False)
680 vbox.pack_start(amount_box, False, False, 5)
682 self.fee_box = fee_box = gtk.HBox()
683 fee_label = gtk.Label('Fee:')
684 fee_label.set_size_request(100,-1)
685 fee_box.pack_start(fee_label, False)
686 fee_entry = gtk.Entry()
687 fee_entry.set_size_request(60, 26)
688 fee_box.pack_start(fee_entry, False)
689 vbox.pack_start(fee_box, False, False, 5)
692 empty_label = gtk.Label('')
693 empty_label.set_size_request(100,-1)
694 end_box.pack_start(empty_label, False)
695 send_button = gtk.Button("Send")
697 end_box.pack_start(send_button, False, False, 0)
698 clear_button = gtk.Button("Clear")
700 end_box.pack_start(clear_button, False, False, 15)
701 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
702 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
704 vbox.pack_start(end_box, False, False, 5)
706 # display this line only if there is a signature
707 payto_sig = gtk.HBox()
708 payto_sig_id = gtk.Label('')
709 payto_sig.pack_start(payto_sig_id, False)
710 vbox.pack_start(payto_sig, True, True, 5)
713 self.user_fee = False
715 def entry_changed( entry, is_fee ):
716 self.funds_error = False
717 amount = numbify(amount_entry)
718 fee = numbify(fee_entry)
719 if not is_fee: fee = None
722 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
724 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
727 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
728 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
729 send_button.set_sensitive(True)
731 send_button.set_sensitive(False)
732 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
733 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
734 self.funds_error = True
736 amount_entry.connect('changed', entry_changed, False)
737 fee_entry.connect('changed', entry_changed, True)
739 self.payto_entry = payto_entry
740 self.payto_fee_entry = fee_entry
741 self.payto_sig_id = payto_sig_id
742 self.payto_sig = payto_sig
743 self.amount_entry = amount_entry
744 self.message_entry = message_entry
745 self.add_tab(page, 'Send')
747 def set_frozen(self,entry,frozen):
749 entry.set_editable(False)
750 entry.set_has_frame(False)
751 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
753 entry.set_editable(True)
754 entry.set_has_frame(True)
755 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
757 def set_url(self, url):
758 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
759 self.notebook.set_current_page(1)
760 self.payto_entry.set_text(payto)
761 self.message_entry.set_text(message)
762 self.amount_entry.set_text(amount)
764 self.set_frozen(self.payto_entry,True)
765 self.set_frozen(self.amount_entry,True)
766 self.set_frozen(self.message_entry,True)
767 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
769 self.payto_sig.set_visible(False)
771 def create_about_tab(self):
776 tv.set_editable(False)
777 tv.set_cursor_visible(False)
778 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
779 scroll = gtk.ScrolledWindow()
781 page.pack_start(scroll)
782 self.info = tv.get_buffer()
783 self.add_tab(page, 'Wall')
785 def do_clear(self, w, data):
786 self.payto_sig.set_visible(False)
787 self.payto_fee_entry.set_text('')
788 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
789 self.set_frozen(entry,False)
792 def question(self,msg):
793 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
795 result = dialog.run()
797 return result == gtk.RESPONSE_OK
799 def do_send(self, w, data):
800 payto_entry, label_entry, amount_entry, fee_entry = data
801 label = label_entry.get_text()
802 r = payto_entry.get_text()
805 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
806 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
809 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
813 self.update_sending_tab()
816 to_address = m2.group(5)
820 if not is_valid(to_address):
821 self.show_message( "invalid bitcoin address:\n"+to_address)
825 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
827 self.show_message( "invalid amount")
830 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
832 self.show_message( "invalid fee")
835 if self.wallet.use_encryption:
836 password = password_dialog(self.window)
843 tx = self.wallet.mktx( [(to_address, amount)], password, fee )
844 except BaseException, e:
845 self.show_message(str(e))
848 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
849 self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." )
854 self.wallet.labels[tx.hash()] = label
856 status, msg = self.wallet.sendtx( tx )
858 self.show_message( "payment sent.\n" + msg )
859 payto_entry.set_text("")
860 label_entry.set_text("")
861 amount_entry.set_text("")
862 fee_entry.set_text("")
864 self.update_sending_tab()
866 self.show_message( msg )
869 def treeview_button_press(self, treeview, event):
870 if event.type == gtk.gdk._2BUTTON_PRESS:
871 c = treeview.get_cursor()[0]
872 if treeview == self.history_treeview:
873 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
874 self.show_message(tx_details)
875 elif treeview == self.contacts_treeview:
876 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
877 #a = self.wallet.aliases.get(m)
879 # if a[0] in self.wallet.authorities.keys():
880 # s = self.wallet.authorities.get(a[0])
883 # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
884 # self.show_message(msg)
887 def treeview_key_press(self, treeview, event):
888 c = treeview.get_cursor()[0]
889 if event.keyval == gtk.keysyms.Up:
891 treeview.parent.grab_focus()
892 treeview.set_cursor((0,))
893 elif event.keyval == gtk.keysyms.Return:
894 if treeview == self.history_treeview:
895 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
896 self.show_message(tx_details)
897 elif treeview == self.contacts_treeview:
898 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
899 #a = self.wallet.aliases.get(m)
901 # if a[0] in self.wallet.authorities.keys():
902 # s = self.wallet.authorities.get(a[0])
905 # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
906 # self.show_message(msg)
910 def create_history_tab(self):
912 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
913 treeview = gtk.TreeView(model=self.history_list)
914 self.history_treeview = treeview
915 treeview.set_tooltip_column(7)
917 treeview.connect('key-press-event', self.treeview_key_press)
918 treeview.connect('button-press-event', self.treeview_button_press)
920 tvcolumn = gtk.TreeViewColumn('')
921 treeview.append_column(tvcolumn)
922 cell = gtk.CellRendererPixbuf()
923 tvcolumn.pack_start(cell, False)
924 tvcolumn.set_attributes(cell, stock_id=1)
926 tvcolumn = gtk.TreeViewColumn('Date')
927 treeview.append_column(tvcolumn)
928 cell = gtk.CellRendererText()
929 tvcolumn.pack_start(cell, False)
930 tvcolumn.add_attribute(cell, 'text', 2)
932 tvcolumn = gtk.TreeViewColumn('Description')
933 treeview.append_column(tvcolumn)
934 cell = gtk.CellRendererText()
935 cell.set_property('foreground', 'grey')
936 cell.set_property('family', MONOSPACE_FONT)
937 cell.set_property('editable', True)
938 def edited_cb(cell, path, new_text, h_list):
939 tx = h_list.get_value( h_list.get_iter(path), 0)
940 self.wallet.labels[tx] = new_text
942 self.update_history_tab()
943 cell.connect('edited', edited_cb, self.history_list)
944 def editing_started(cell, entry, path, h_list):
945 tx = h_list.get_value( h_list.get_iter(path), 0)
946 if not self.wallet.labels.get(tx): entry.set_text('')
947 cell.connect('editing-started', editing_started, self.history_list)
948 tvcolumn.set_expand(True)
949 tvcolumn.pack_start(cell, True)
950 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
952 tvcolumn = gtk.TreeViewColumn('Amount')
953 treeview.append_column(tvcolumn)
954 cell = gtk.CellRendererText()
955 cell.set_alignment(1, 0.5)
956 cell.set_property('family', MONOSPACE_FONT)
957 tvcolumn.pack_start(cell, False)
958 tvcolumn.add_attribute(cell, 'text', 5)
960 tvcolumn = gtk.TreeViewColumn('Balance')
961 treeview.append_column(tvcolumn)
962 cell = gtk.CellRendererText()
963 cell.set_alignment(1, 0.5)
964 cell.set_property('family', MONOSPACE_FONT)
965 tvcolumn.pack_start(cell, False)
966 tvcolumn.add_attribute(cell, 'text', 6)
968 tvcolumn = gtk.TreeViewColumn('Tooltip')
969 treeview.append_column(tvcolumn)
970 cell = gtk.CellRendererText()
971 tvcolumn.pack_start(cell, False)
972 tvcolumn.add_attribute(cell, 'text', 7)
973 tvcolumn.set_visible(False)
975 scroll = gtk.ScrolledWindow()
976 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
979 self.add_tab(scroll, 'History')
980 self.update_history_tab()
983 def create_recv_tab(self):
984 self.recv_list = gtk.ListStore(str, str, str)
985 self.add_tab( self.make_address_list(True), 'Receive')
986 self.update_receiving_tab()
988 def create_book_tab(self):
989 self.addressbook_list = gtk.ListStore(str, str, str)
990 self.add_tab( self.make_address_list(False), 'Contacts')
991 self.update_sending_tab()
993 def make_address_list(self, is_recv):
994 liststore = self.recv_list if is_recv else self.addressbook_list
995 treeview = gtk.TreeView(model= liststore)
996 treeview.connect('key-press-event', self.treeview_key_press)
997 treeview.connect('button-press-event', self.treeview_button_press)
1000 self.contacts_treeview = treeview
1002 tvcolumn = gtk.TreeViewColumn('Address')
1003 treeview.append_column(tvcolumn)
1004 cell = gtk.CellRendererText()
1005 cell.set_property('family', MONOSPACE_FONT)
1006 tvcolumn.pack_start(cell, True)
1007 tvcolumn.add_attribute(cell, 'text', 0)
1009 tvcolumn = gtk.TreeViewColumn('Label')
1010 tvcolumn.set_expand(True)
1011 treeview.append_column(tvcolumn)
1012 cell = gtk.CellRendererText()
1013 cell.set_property('editable', True)
1014 def edited_cb2(cell, path, new_text, liststore):
1015 address = liststore.get_value( liststore.get_iter(path), 0)
1016 self.wallet.labels[address] = new_text
1018 self.update_receiving_tab()
1019 self.update_sending_tab()
1020 self.update_history_tab()
1021 cell.connect('edited', edited_cb2, liststore)
1022 tvcolumn.pack_start(cell, True)
1023 tvcolumn.add_attribute(cell, 'text', 1)
1025 tvcolumn = gtk.TreeViewColumn('Tx')
1026 treeview.append_column(tvcolumn)
1027 cell = gtk.CellRendererText()
1028 tvcolumn.pack_start(cell, True)
1029 tvcolumn.add_attribute(cell, 'text', 2)
1031 scroll = gtk.ScrolledWindow()
1032 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1033 scroll.add(treeview)
1037 button = gtk.Button("New")
1038 button.connect("clicked", self.newaddress_dialog)
1040 hbox.pack_start(button,False)
1042 def showqrcode(w, treeview, liststore):
1043 path, col = treeview.get_cursor()
1045 address = liststore.get_value(liststore.get_iter(path), 0)
1046 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1050 size = qr.getModuleCount()*boxsize
1051 def area_expose_cb(area, event):
1052 style = area.get_style()
1053 k = qr.getModuleCount()
1056 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1057 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1058 area = gtk.DrawingArea()
1059 area.set_size_request(size, size)
1060 area.connect("expose-event", area_expose_cb)
1062 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1063 dialog.vbox.add(area)
1067 button = gtk.Button("QR")
1068 button.connect("clicked", showqrcode, treeview, liststore)
1070 hbox.pack_start(button,False)
1072 button = gtk.Button("Copy to clipboard")
1073 def copy2clipboard(w, treeview, liststore):
1075 path, col = treeview.get_cursor()
1077 address = liststore.get_value( liststore.get_iter(path), 0)
1078 if platform.system() == 'Windows':
1079 from Tkinter import Tk
1083 r.clipboard_append( address )
1086 c = gtk.clipboard_get()
1087 c.set_text( address )
1088 button.connect("clicked", copy2clipboard, treeview, liststore)
1090 hbox.pack_start(button,False)
1093 button = gtk.Button("Pay to")
1094 def payto(w, treeview, liststore):
1095 path, col = treeview.get_cursor()
1097 address = liststore.get_value( liststore.get_iter(path), 0)
1098 self.payto_entry.set_text( address )
1099 self.notebook.set_current_page(1)
1100 self.amount_entry.grab_focus()
1102 button.connect("clicked", payto, treeview, liststore)
1104 hbox.pack_start(button,False)
1107 vbox.pack_start(scroll,True)
1108 vbox.pack_start(hbox, False)
1111 def update_status_bar(self):
1112 interface = self.wallet.interface
1113 if self.funds_error:
1114 text = "Not enough funds"
1115 elif interface and interface.is_connected:
1116 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1117 if not self.wallet.up_to_date:
1118 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1119 text = "Synchronizing..."
1121 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1122 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1123 c, u = self.wallet.get_balance()
1124 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1125 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1127 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1128 self.network_button.set_tooltip_text("Not connected.")
1129 text = "Not connected"
1131 self.status_bar.pop(self.context_id)
1132 self.status_bar.push(self.context_id, text)
1134 if self.wallet.up_to_date and self.wallet_updated:
1135 self.update_history_tab()
1136 self.update_receiving_tab()
1137 # addressbook too...
1138 self.info.set_text( self.wallet.interface.banner )
1139 self.wallet_updated = False
1141 def update_receiving_tab(self):
1142 self.recv_list.clear()
1143 for address in self.wallet.addresses(True):
1144 if self.wallet.is_change(address):continue
1145 label = self.wallet.labels.get(address)
1146 h = self.wallet.history.get(address,[])
1148 tx = "None" if n==0 else "%d"%n
1149 self.recv_list.append((address, label, tx ))
1151 def update_sending_tab(self):
1152 # detect addresses that are not mine in history, add them here...
1153 self.addressbook_list.clear()
1154 #for alias, v in self.wallet.aliases.items():
1156 # label = self.wallet.labels.get(alias)
1157 # self.addressbook_list.append((alias, label, '-'))
1159 for address in self.wallet.addressbook:
1160 label = self.wallet.labels.get(address)
1161 n = self.wallet.get_num_tx(address)
1162 self.addressbook_list.append((address, label, "%d"%n))
1164 def update_history_tab(self):
1165 cursor = self.history_treeview.get_cursor()[0]
1166 self.history_list.clear()
1168 for item in self.wallet.get_tx_history():
1169 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1172 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1175 conf_icon = gtk.STOCK_APPLY
1177 time_str = 'unverified'
1180 time_str = 'pending'
1181 conf_icon = gtk.STOCK_EXECUTE
1183 label, is_default_label = self.wallet.get_label(tx_hash)
1184 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1185 details = self.get_tx_details(tx_hash)
1187 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1188 format_satoshis(value,True,self.wallet.num_zeros),
1189 format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1190 if cursor: self.history_treeview.set_cursor( cursor )
1193 def get_tx_details(self, tx_hash):
1195 if not tx_hash: return ''
1196 tx = self.wallet.transactions.get(tx_hash)
1197 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
1198 conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1201 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1203 time_str = 'pending'
1205 inputs = map(lambda x: x.get('address'), tx.inputs)
1206 outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1207 tx_details = "Transaction Details" +"\n\n" \
1208 + "Transaction ID:\n" + tx_hash + "\n\n" \
1209 + "Status: %d confirmations\n"%conf
1212 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1213 + "Transaction fee: %s\n"% format_satoshis(fee, False)
1215 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1216 + "Transaction fee: unknown\n"
1218 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1220 tx_details += "Date: %s\n\n"%time_str \
1221 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1222 + "Outputs:\n-"+ '\n-'.join(outputs)
1228 def newaddress_dialog(self, w):
1230 title = "New Contact"
1231 dialog = gtk.Dialog(title, parent=self.window,
1232 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1233 buttons= ("cancel", 0, "ok",1) )
1237 label_label = gtk.Label('Label:')
1238 label_label.set_size_request(120,10)
1240 label.pack_start(label_label)
1241 label_entry = gtk.Entry()
1243 label.pack_start(label_entry)
1245 dialog.vbox.pack_start(label, False, True, 5)
1247 address = gtk.HBox()
1248 address_label = gtk.Label('Address:')
1249 address_label.set_size_request(120,10)
1250 address_label.show()
1251 address.pack_start(address_label)
1252 address_entry = gtk.Entry()
1253 address_entry.show()
1254 address.pack_start(address_entry)
1256 dialog.vbox.pack_start(address, False, True, 5)
1258 result = dialog.run()
1259 address = address_entry.get_text()
1260 label = label_entry.get_text()
1264 if is_valid(address):
1265 self.wallet.addressbook.append(address)
1266 if label: self.wallet.labels[address] = label
1268 self.update_sending_tab()
1270 errorDialog = gtk.MessageDialog(
1272 flags=gtk.DIALOG_MODAL,
1273 buttons= gtk.BUTTONS_CLOSE,
1274 message_format = "Invalid address")
1277 errorDialog.destroy()
1281 class ElectrumGui():
1283 def __init__(self, wallet, config):
1284 self.wallet = wallet
1285 self.config = config
1287 def main(self, url=None):
1288 ew = ElectrumWindow(self.wallet, self.config)
1289 if url: ew.set_url(url)
1292 def restore_or_create(self):
1293 return restore_create_dialog(self.wallet)
1295 def seed_dialog(self):
1296 return run_recovery_dialog( self.wallet )
1298 def verify_seed(self):
1299 self.wallet.save_seed()
1302 def network_dialog(self):
1303 return run_network_dialog( self.wallet, parent=None )
1305 def show_seed(self):
1306 show_seed_dialog(self.wallet, None, None)
1308 def password_dialog(self):
1309 change_password_dialog(self.wallet, None, None)
1311 def restore_wallet(self):
1312 wallet = self.wallet
1314 dialog = gtk.MessageDialog(
1316 flags = gtk.DIALOG_MODAL,
1317 buttons = gtk.BUTTONS_CANCEL,
1318 message_format = "Please wait..." )
1321 def recover_thread( wallet, dialog ):
1322 while not wallet.is_up_to_date():
1324 gobject.idle_add( dialog.destroy )
1326 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1329 if r==gtk.RESPONSE_CANCEL: return False
1330 if not wallet.is_found():
1331 show_message("No transactions found for this seed")