3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import thread, time, ast, sys, re
21 import socket, traceback
25 from decimal import Decimal
26 from util import print_error
28 import pyqrnative, mnemonic
30 gtk.gdk.threads_init()
33 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
35 from wallet import format_satoshis
36 from interface import DEFAULT_SERVERS
38 def numbify(entry, is_int = False):
39 text = entry.get_text().strip()
41 if not is_int: chars +='.'
42 s = ''.join([i for i in text if i in chars])
47 s = s[:p] + '.' + s[p:p+8]
49 amount = int( Decimal(s) * 100000000 )
63 def show_seed_dialog(wallet, password, parent):
65 show_message("No seed")
68 seed = wallet.pw_decode( wallet.seed, password)
70 show_message("Incorrect password")
72 dialog = gtk.MessageDialog(
74 flags = gtk.DIALOG_MODAL,
75 buttons = gtk.BUTTONS_OK,
76 message_format = "Your wallet generation seed is:\n\n" + seed \
77 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
78 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
79 dialog.set_title("Seed")
84 def restore_create_dialog(wallet):
86 # ask if the user wants to create a new wallet, or recover from a seed.
87 # if he wants to recover, and nothing is found, do not create wallet
88 dialog = gtk.Dialog("electrum", parent=None,
89 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
90 buttons= ("create", 0, "restore",1, "cancel",2) )
92 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
94 dialog.vbox.pack_start(label)
103 # ask for the server.
104 if not run_network_dialog( wallet, parent=None ): return False
108 wallet.new_seed(None)
110 wallet.init_mpk( wallet.seed )
111 wallet.up_to_date_event.clear()
114 # run a dialog indicating the seed, ask the user to remember it
115 show_seed_dialog(wallet, None, None)
118 change_password_dialog(wallet, None, None)
120 # ask for seed and gap.
121 run_recovery_dialog( wallet )
123 dialog = gtk.MessageDialog(
125 flags = gtk.DIALOG_MODAL,
126 buttons = gtk.BUTTONS_CANCEL,
127 message_format = "Please wait..." )
130 def recover_thread( wallet, dialog ):
131 wallet.init_mpk( wallet.seed ) # not encrypted at this point
132 wallet.up_to_date_event.clear()
135 if wallet.is_found():
136 # history and addressbook
137 wallet.update_tx_history()
138 wallet.fill_addressbook()
139 print "Recovery successful"
141 gobject.idle_add( dialog.destroy )
143 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
146 if r==gtk.RESPONSE_CANCEL: return False
147 if not wallet.is_found:
148 show_message("No transactions found for this seed")
154 def run_recovery_dialog(wallet):
155 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
156 dialog = gtk.MessageDialog(
158 flags = gtk.DIALOG_MODAL,
159 buttons = gtk.BUTTONS_OK_CANCEL,
160 message_format = message)
163 dialog.set_default_response(gtk.RESPONSE_OK)
165 # ask seed, server and gap in the same dialog
166 seed_box = gtk.HBox()
167 seed_label = gtk.Label('Seed or mnemonic:')
168 seed_label.set_size_request(150,-1)
169 seed_box.pack_start(seed_label, False, False, 10)
171 seed_entry = gtk.Entry()
173 seed_entry.set_size_request(450,-1)
174 seed_box.pack_start(seed_entry, False, False, 10)
175 add_help_button(seed_box, '.')
177 vbox.pack_start(seed_box, False, False, 5)
180 gap_label = gtk.Label('Gap limit:')
181 gap_label.set_size_request(150,10)
183 gap.pack_start(gap_label,False, False, 10)
184 gap_entry = gtk.Entry()
185 gap_entry.set_text("%d"%wallet.gap_limit)
186 gap_entry.connect('changed', numbify, True)
188 gap.pack_start(gap_entry,False,False, 10)
189 add_help_button(gap, 'The maximum gap that is allowed between unused addresses in your wallet. During wallet recovery, this parameter is used to decide when to stop the recovery process. If you increase this value, you will need to remember it in order to be able to recover your wallet from seed.')
191 vbox.pack_start(gap, False,False, 5)
195 gap = gap_entry.get_text()
196 seed = seed_entry.get_text()
199 if r==gtk.RESPONSE_CANCEL:
204 show_message("error")
210 print_error("Warning: Not hex, trying decode")
211 seed = mnemonic.mn_decode( seed.split(' ') )
213 show_message("no seed")
217 wallet.gap_limit = gap
222 def run_settings_dialog(wallet, parent):
224 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
226 dialog = gtk.MessageDialog(
228 flags = gtk.DIALOG_MODAL,
229 buttons = gtk.BUTTONS_OK_CANCEL,
230 message_format = message)
233 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
235 dialog.set_image(image)
236 dialog.set_title("Settings")
239 dialog.set_default_response(gtk.RESPONSE_OK)
242 fee_entry = gtk.Entry()
243 fee_label = gtk.Label('Transaction fee:')
244 fee_label.set_size_request(150,10)
246 fee.pack_start(fee_label,False, False, 10)
247 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
248 fee_entry.connect('changed', numbify, False)
250 fee.pack_start(fee_entry,False,False, 10)
251 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
253 vbox.pack_start(fee, False,False, 5)
256 nz_entry = gtk.Entry()
257 nz_label = gtk.Label('Display zeros:')
258 nz_label.set_size_request(150,10)
260 nz.pack_start(nz_label,False, False, 10)
261 nz_entry.set_text( str( wallet.num_zeros ))
262 nz_entry.connect('changed', numbify, True)
264 nz.pack_start(nz_entry,False,False, 10)
265 add_help_button(nz, "Number of zeros displayed after the decimal point.\nFor example, if this number is 2, then '5.' is displayed as '5.00'")
267 vbox.pack_start(nz, False,False, 5)
271 fee = fee_entry.get_text()
272 nz = nz_entry.get_text()
275 if r==gtk.RESPONSE_CANCEL:
279 fee = int( 100000000 * Decimal(fee) )
281 show_message("error")
283 if wallet.fee != fee:
291 show_message("error")
293 if wallet.num_zeros != nz:
294 wallet.num_zeros = nz
300 def run_network_dialog( wallet, parent ):
302 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
303 interface = wallet.interface
305 if interface.is_connected:
306 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
308 status = "Not connected"
309 server = wallet.server
312 status = "Please choose a server."
313 server = random.choice( DEFAULT_SERVERS )
315 if not wallet.interface.servers:
317 for x in DEFAULT_SERVERS:
318 h,port,protocol = x.split(':')
319 servers_list.append( (h,[(protocol,port)] ) )
321 servers_list = wallet.interface.servers
324 for item in servers_list:
328 protocol, port = item2
332 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
333 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
334 dialog.set_title("Server")
335 dialog.set_image(image)
339 host_box = gtk.HBox()
340 host_label = gtk.Label('Connect to:')
341 host_label.set_size_request(100,-1)
343 host_box.pack_start(host_label, False, False, 10)
344 host_entry = gtk.Entry()
345 host_entry.set_size_request(200,-1)
346 host_entry.set_text(server)
348 host_box.pack_start(host_entry, False, False, 10)
349 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)')
353 p_box = gtk.HBox(False, 10)
356 p_label = gtk.Label('Protocol:')
357 p_label.set_size_request(100,-1)
359 p_box.pack_start(p_label, False, False, 10)
361 radio1 = gtk.RadioButton(None, "tcp")
362 p_box.pack_start(radio1, True, True, 0)
364 radio2 = gtk.RadioButton(radio1, "http")
365 p_box.pack_start(radio2, True, True, 0)
369 return unicode(host_entry.get_text()).split(':')
371 def set_button(protocol):
374 elif protocol == 'h':
377 def set_protocol(protocol):
378 host = current_line()[0]
380 if protocol not in pp.keys():
381 protocol = pp.keys()[0]
384 host_entry.set_text( host + ':' + port + ':' + protocol)
386 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
387 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
389 server_list = gtk.ListStore(str)
390 for host in plist.keys():
391 server_list.append([host])
393 treeview = gtk.TreeView(model=server_list)
396 if wallet.interface.servers:
397 label = 'Active Servers'
399 label = 'Default Servers'
401 tvcolumn = gtk.TreeViewColumn(label)
402 treeview.append_column(tvcolumn)
403 cell = gtk.CellRendererText()
404 tvcolumn.pack_start(cell, False)
405 tvcolumn.add_attribute(cell, 'text', 0)
407 scroll = gtk.ScrolledWindow()
408 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
412 vbox.pack_start(host_box, False,False, 5)
413 vbox.pack_start(p_box, True, True, 0)
414 vbox.pack_start(scroll)
416 def my_treeview_cb(treeview):
417 path, view_column = treeview.get_cursor()
418 host = server_list.get_value( server_list.get_iter(path), 0)
424 protocol = pp.keys()[0]
426 host_entry.set_text( host + ':' + port + ':' + protocol)
429 treeview.connect('cursor-changed', my_treeview_cb)
433 server = host_entry.get_text()
436 if r==gtk.RESPONSE_CANCEL:
440 wallet.set_server(server)
442 show_message("error:" + server)
451 def show_message(message, parent=None):
452 dialog = gtk.MessageDialog(
454 flags = gtk.DIALOG_MODAL,
455 buttons = gtk.BUTTONS_CLOSE,
456 message_format = message )
461 def password_line(label):
462 password = gtk.HBox()
463 password_label = gtk.Label(label)
464 password_label.set_size_request(120,10)
465 password_label.show()
466 password.pack_start(password_label,False, False, 10)
467 password_entry = gtk.Entry()
468 password_entry.set_size_request(300,-1)
469 password_entry.set_visibility(False)
470 password_entry.show()
471 password.pack_start(password_entry,False,False, 10)
473 return password, password_entry
475 def password_dialog(parent):
476 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
477 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
478 dialog.get_image().set_visible(False)
479 current_pw, current_pw_entry = password_line('Password:')
480 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
481 dialog.vbox.pack_start(current_pw, False, True, 0)
483 result = dialog.run()
484 pw = current_pw_entry.get_text()
486 if result != gtk.RESPONSE_CANCEL: return pw
488 def change_password_dialog(wallet, parent, icon):
490 show_message("No seed")
494 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'
496 msg = "Please choose a password to encrypt your wallet keys"
498 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
499 dialog.set_title("Change password")
501 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
503 dialog.set_image(image)
505 if wallet.use_encryption:
506 current_pw, current_pw_entry = password_line('Current password:')
507 dialog.vbox.pack_start(current_pw, False, True, 0)
509 password, password_entry = password_line('New password:')
510 dialog.vbox.pack_start(password, False, True, 5)
511 password2, password2_entry = password_line('Confirm password:')
512 dialog.vbox.pack_start(password2, False, True, 5)
515 result = dialog.run()
516 password = current_pw_entry.get_text() if wallet.use_encryption else None
517 new_password = password_entry.get_text()
518 new_password2 = password2_entry.get_text()
520 if result == gtk.RESPONSE_CANCEL:
524 seed = wallet.pw_decode( wallet.seed, password)
526 show_message("Incorrect password")
529 if new_password != new_password2:
530 show_message("passwords do not match")
533 wallet.update_password(seed, password, new_password)
536 if wallet.use_encryption:
537 icon.set_tooltip_text('wallet is encrypted')
539 icon.set_tooltip_text('wallet is unencrypted')
542 def add_help_button(hbox, message):
543 button = gtk.Button('?')
544 button.connect("clicked", lambda x: show_message(message))
546 hbox.pack_start(button,False, False)
549 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
551 gobject.type_register(MyWindow)
552 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
553 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
556 class ElectrumWindow:
558 def show_message(self, msg):
559 show_message(msg, self.window)
561 def __init__(self, wallet, config):
564 self.funds_error = False # True if not enough funds
566 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
567 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
568 if not self.wallet.seed: title += ' [seedless]'
569 self.window.set_title(title)
570 self.window.connect("destroy", gtk.main_quit)
571 self.window.set_border_width(0)
572 self.window.connect('mykeypress', gtk.main_quit)
573 self.window.set_default_size(720, 350)
577 self.notebook = gtk.Notebook()
578 self.create_history_tab()
580 self.create_send_tab()
581 self.create_recv_tab()
582 self.create_book_tab()
583 self.create_about_tab()
585 vbox.pack_start(self.notebook, True, True, 2)
587 self.status_bar = gtk.Statusbar()
588 vbox.pack_start(self.status_bar, False, False, 0)
590 self.status_image = gtk.Image()
591 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
592 self.status_image.set_alignment(True, 0.5 )
593 self.status_image.show()
595 self.network_button = gtk.Button()
596 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
597 self.network_button.add(self.status_image)
598 self.network_button.set_relief(gtk.RELIEF_NONE)
599 self.network_button.show()
600 self.status_bar.pack_end(self.network_button, False, False)
603 def seedb(w, wallet):
604 if wallet.use_encryption:
605 password = password_dialog(self.window)
606 if not password: return
607 else: password = None
608 show_seed_dialog(wallet, password, self.window)
609 button = gtk.Button('S')
610 button.connect("clicked", seedb, wallet )
611 button.set_relief(gtk.RELIEF_NONE)
613 self.status_bar.pack_end(button,False, False)
615 settings_icon = gtk.Image()
616 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
617 settings_icon.set_alignment(0.5, 0.5)
618 settings_icon.set_size_request(16,16 )
621 prefs_button = gtk.Button()
622 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
623 prefs_button.add(settings_icon)
624 prefs_button.set_tooltip_text("Settings")
625 prefs_button.set_relief(gtk.RELIEF_NONE)
627 self.status_bar.pack_end(prefs_button,False,False)
629 pw_icon = gtk.Image()
630 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
631 pw_icon.set_alignment(0.5, 0.5)
632 pw_icon.set_size_request(16,16 )
636 password_button = gtk.Button()
637 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
638 password_button.add(pw_icon)
639 password_button.set_relief(gtk.RELIEF_NONE)
640 password_button.show()
641 self.status_bar.pack_end(password_button,False,False)
643 self.window.add(vbox)
644 self.window.show_all()
647 self.context_id = self.status_bar.get_context_id("statusbar")
648 self.update_status_bar()
650 def update_status_bar_thread():
652 gobject.idle_add( self.update_status_bar )
656 def check_recipient_thread():
660 if self.payto_entry.is_focus():
662 r = self.payto_entry.get_text()
666 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
668 to_address = self.wallet.get_alias(r, interactive=False)
672 s = r + ' <' + to_address + '>'
673 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
676 thread.start_new_thread(update_status_bar_thread, ())
678 thread.start_new_thread(check_recipient_thread, ())
679 self.notebook.set_current_page(0)
682 def add_tab(self, page, name):
683 tab_label = gtk.Label(name)
685 self.notebook.append_page(page, tab_label)
688 def create_send_tab(self):
690 page = vbox = gtk.VBox()
694 payto_label = gtk.Label('Pay to:')
695 payto_label.set_size_request(100,-1)
696 payto.pack_start(payto_label, False)
697 payto_entry = gtk.Entry()
698 payto_entry.set_size_request(450, 26)
699 payto.pack_start(payto_entry, False)
700 vbox.pack_start(payto, False, False, 5)
703 message_label = gtk.Label('Description:')
704 message_label.set_size_request(100,-1)
705 message.pack_start(message_label, False)
706 message_entry = gtk.Entry()
707 message_entry.set_size_request(450, 26)
708 message.pack_start(message_entry, False)
709 vbox.pack_start(message, False, False, 5)
711 amount_box = gtk.HBox()
712 amount_label = gtk.Label('Amount:')
713 amount_label.set_size_request(100,-1)
714 amount_box.pack_start(amount_label, False)
715 amount_entry = gtk.Entry()
716 amount_entry.set_size_request(120, -1)
717 amount_box.pack_start(amount_entry, False)
718 vbox.pack_start(amount_box, False, False, 5)
720 self.fee_box = fee_box = gtk.HBox()
721 fee_label = gtk.Label('Fee:')
722 fee_label.set_size_request(100,-1)
723 fee_box.pack_start(fee_label, False)
724 fee_entry = gtk.Entry()
725 fee_entry.set_size_request(60, 26)
726 fee_box.pack_start(fee_entry, False)
727 vbox.pack_start(fee_box, False, False, 5)
730 empty_label = gtk.Label('')
731 empty_label.set_size_request(100,-1)
732 end_box.pack_start(empty_label, False)
733 send_button = gtk.Button("Send")
735 end_box.pack_start(send_button, False, False, 0)
736 clear_button = gtk.Button("Clear")
738 end_box.pack_start(clear_button, False, False, 15)
739 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
740 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
742 vbox.pack_start(end_box, False, False, 5)
744 # display this line only if there is a signature
745 payto_sig = gtk.HBox()
746 payto_sig_id = gtk.Label('')
747 payto_sig.pack_start(payto_sig_id, False)
748 vbox.pack_start(payto_sig, True, True, 5)
751 self.user_fee = False
753 def entry_changed( entry, is_fee ):
754 self.funds_error = False
755 amount = numbify(amount_entry)
756 fee = numbify(fee_entry)
757 if not is_fee: fee = None
760 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
762 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
765 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
766 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
767 send_button.set_sensitive(True)
769 send_button.set_sensitive(False)
770 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
771 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
772 self.funds_error = True
774 amount_entry.connect('changed', entry_changed, False)
775 fee_entry.connect('changed', entry_changed, True)
777 self.payto_entry = payto_entry
778 self.payto_fee_entry = fee_entry
779 self.payto_sig_id = payto_sig_id
780 self.payto_sig = payto_sig
781 self.amount_entry = amount_entry
782 self.message_entry = message_entry
783 self.add_tab(page, 'Send')
785 def set_frozen(self,entry,frozen):
787 entry.set_editable(False)
788 entry.set_has_frame(False)
789 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
791 entry.set_editable(True)
792 entry.set_has_frame(True)
793 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
795 def set_url(self, url):
796 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
797 self.notebook.set_current_page(1)
798 self.payto_entry.set_text(payto)
799 self.message_entry.set_text(message)
800 self.amount_entry.set_text(amount)
802 self.set_frozen(self.payto_entry,True)
803 self.set_frozen(self.amount_entry,True)
804 self.set_frozen(self.message_entry,True)
805 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
807 self.payto_sig.set_visible(False)
809 def create_about_tab(self):
814 tv.set_editable(False)
815 tv.set_cursor_visible(False)
816 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
818 self.info = tv.get_buffer()
819 self.add_tab(page, 'Wall')
821 def do_clear(self, w, data):
822 self.payto_sig.set_visible(False)
823 self.payto_fee_entry.set_text('')
824 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
825 self.set_frozen(entry,False)
828 def question(self,msg):
829 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
831 result = dialog.run()
833 return result == gtk.RESPONSE_OK
835 def do_send(self, w, data):
836 payto_entry, label_entry, amount_entry, fee_entry = data
837 label = label_entry.get_text()
838 r = payto_entry.get_text()
841 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
842 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
845 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
849 self.update_sending_tab()
852 to_address = m2.group(5)
856 if not self.wallet.is_valid(to_address):
857 self.show_message( "invalid bitcoin address:\n"+to_address)
861 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
863 self.show_message( "invalid amount")
866 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
868 self.show_message( "invalid fee")
871 if self.wallet.use_encryption:
872 password = password_dialog(self.window)
879 tx = self.wallet.mktx( to_address, amount, label, password, fee )
880 except BaseException, e:
881 self.show_message(str(e))
884 status, msg = self.wallet.sendtx( tx )
886 self.show_message( "payment sent.\n" + msg )
887 payto_entry.set_text("")
888 label_entry.set_text("")
889 amount_entry.set_text("")
890 fee_entry.set_text("")
892 self.update_sending_tab()
894 self.show_message( msg )
897 def treeview_button_press(self, treeview, event):
898 if event.type == gtk.gdk._2BUTTON_PRESS:
899 c = treeview.get_cursor()[0]
900 if treeview == self.history_treeview:
901 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
902 self.show_message(tx_details)
903 elif treeview == self.contacts_treeview:
904 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
905 a = self.wallet.aliases.get(m)
907 if a[0] in self.wallet.authorities.keys():
908 s = self.wallet.authorities.get(a[0])
911 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
912 self.show_message(msg)
915 def treeview_key_press(self, treeview, event):
916 c = treeview.get_cursor()[0]
917 if event.keyval == gtk.keysyms.Up:
919 treeview.parent.grab_focus()
920 treeview.set_cursor((0,))
921 elif event.keyval == gtk.keysyms.Return:
922 if treeview == self.history_treeview:
923 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
924 self.show_message(tx_details)
925 elif treeview == self.contacts_treeview:
926 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
927 a = self.wallet.aliases.get(m)
929 if a[0] in self.wallet.authorities.keys():
930 s = self.wallet.authorities.get(a[0])
933 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
934 self.show_message(msg)
938 def create_history_tab(self):
940 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
941 treeview = gtk.TreeView(model=self.history_list)
942 self.history_treeview = treeview
943 treeview.set_tooltip_column(7)
945 treeview.connect('key-press-event', self.treeview_key_press)
946 treeview.connect('button-press-event', self.treeview_button_press)
948 tvcolumn = gtk.TreeViewColumn('')
949 treeview.append_column(tvcolumn)
950 cell = gtk.CellRendererPixbuf()
951 tvcolumn.pack_start(cell, False)
952 tvcolumn.set_attributes(cell, stock_id=1)
954 tvcolumn = gtk.TreeViewColumn('Date')
955 treeview.append_column(tvcolumn)
956 cell = gtk.CellRendererText()
957 tvcolumn.pack_start(cell, False)
958 tvcolumn.add_attribute(cell, 'text', 2)
960 tvcolumn = gtk.TreeViewColumn('Description')
961 treeview.append_column(tvcolumn)
962 cell = gtk.CellRendererText()
963 cell.set_property('foreground', 'grey')
964 cell.set_property('family', MONOSPACE_FONT)
965 cell.set_property('editable', True)
966 def edited_cb(cell, path, new_text, h_list):
967 tx = h_list.get_value( h_list.get_iter(path), 0)
968 self.wallet.labels[tx] = new_text
970 self.update_history_tab()
971 cell.connect('edited', edited_cb, self.history_list)
972 def editing_started(cell, entry, path, h_list):
973 tx = h_list.get_value( h_list.get_iter(path), 0)
974 if not self.wallet.labels.get(tx): entry.set_text('')
975 cell.connect('editing-started', editing_started, self.history_list)
976 tvcolumn.set_expand(True)
977 tvcolumn.pack_start(cell, True)
978 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
980 tvcolumn = gtk.TreeViewColumn('Amount')
981 treeview.append_column(tvcolumn)
982 cell = gtk.CellRendererText()
983 cell.set_alignment(1, 0.5)
984 cell.set_property('family', MONOSPACE_FONT)
985 tvcolumn.pack_start(cell, False)
986 tvcolumn.add_attribute(cell, 'text', 5)
988 tvcolumn = gtk.TreeViewColumn('Balance')
989 treeview.append_column(tvcolumn)
990 cell = gtk.CellRendererText()
991 cell.set_alignment(1, 0.5)
992 cell.set_property('family', MONOSPACE_FONT)
993 tvcolumn.pack_start(cell, False)
994 tvcolumn.add_attribute(cell, 'text', 6)
996 tvcolumn = gtk.TreeViewColumn('Tooltip')
997 treeview.append_column(tvcolumn)
998 cell = gtk.CellRendererText()
999 tvcolumn.pack_start(cell, False)
1000 tvcolumn.add_attribute(cell, 'text', 7)
1001 tvcolumn.set_visible(False)
1003 scroll = gtk.ScrolledWindow()
1004 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1005 scroll.add(treeview)
1007 self.add_tab(scroll, 'History')
1008 self.update_history_tab()
1011 def create_recv_tab(self):
1012 self.recv_list = gtk.ListStore(str, str, str)
1013 self.add_tab( self.make_address_list(True), 'Receive')
1014 self.update_receiving_tab()
1016 def create_book_tab(self):
1017 self.addressbook_list = gtk.ListStore(str, str, str)
1018 self.add_tab( self.make_address_list(False), 'Contacts')
1019 self.update_sending_tab()
1021 def make_address_list(self, is_recv):
1022 liststore = self.recv_list if is_recv else self.addressbook_list
1023 treeview = gtk.TreeView(model= liststore)
1024 treeview.connect('key-press-event', self.treeview_key_press)
1025 treeview.connect('button-press-event', self.treeview_button_press)
1028 self.contacts_treeview = treeview
1030 tvcolumn = gtk.TreeViewColumn('Address')
1031 treeview.append_column(tvcolumn)
1032 cell = gtk.CellRendererText()
1033 cell.set_property('family', MONOSPACE_FONT)
1034 tvcolumn.pack_start(cell, True)
1035 tvcolumn.add_attribute(cell, 'text', 0)
1037 tvcolumn = gtk.TreeViewColumn('Label')
1038 tvcolumn.set_expand(True)
1039 treeview.append_column(tvcolumn)
1040 cell = gtk.CellRendererText()
1041 cell.set_property('editable', True)
1042 def edited_cb2(cell, path, new_text, liststore):
1043 address = liststore.get_value( liststore.get_iter(path), 0)
1044 self.wallet.labels[address] = new_text
1046 self.wallet.update_tx_labels()
1047 self.update_receiving_tab()
1048 self.update_sending_tab()
1049 self.update_history_tab()
1050 cell.connect('edited', edited_cb2, liststore)
1051 tvcolumn.pack_start(cell, True)
1052 tvcolumn.add_attribute(cell, 'text', 1)
1054 tvcolumn = gtk.TreeViewColumn('Tx')
1055 treeview.append_column(tvcolumn)
1056 cell = gtk.CellRendererText()
1057 tvcolumn.pack_start(cell, True)
1058 tvcolumn.add_attribute(cell, 'text', 2)
1060 scroll = gtk.ScrolledWindow()
1061 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1062 scroll.add(treeview)
1066 button = gtk.Button("New")
1067 button.connect("clicked", self.newaddress_dialog)
1069 hbox.pack_start(button,False)
1071 def showqrcode(w, treeview, liststore):
1072 path, col = treeview.get_cursor()
1074 address = liststore.get_value(liststore.get_iter(path), 0)
1075 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1079 size = qr.getModuleCount()*boxsize
1080 def area_expose_cb(area, event):
1081 style = area.get_style()
1082 k = qr.getModuleCount()
1085 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1086 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1087 area = gtk.DrawingArea()
1088 area.set_size_request(size, size)
1089 area.connect("expose-event", area_expose_cb)
1091 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1092 dialog.vbox.add(area)
1096 button = gtk.Button("QR")
1097 button.connect("clicked", showqrcode, treeview, liststore)
1099 hbox.pack_start(button,False)
1101 button = gtk.Button("Copy to clipboard")
1102 def copy2clipboard(w, treeview, liststore):
1104 path, col = treeview.get_cursor()
1106 address = liststore.get_value( liststore.get_iter(path), 0)
1107 if platform.system() == 'Windows':
1108 from Tkinter import Tk
1112 r.clipboard_append( address )
1115 c = gtk.clipboard_get()
1116 c.set_text( address )
1117 button.connect("clicked", copy2clipboard, treeview, liststore)
1119 hbox.pack_start(button,False)
1122 button = gtk.Button("Pay to")
1123 def payto(w, treeview, liststore):
1124 path, col = treeview.get_cursor()
1126 address = liststore.get_value( liststore.get_iter(path), 0)
1127 self.payto_entry.set_text( address )
1128 self.notebook.set_current_page(1)
1129 self.amount_entry.grab_focus()
1131 button.connect("clicked", payto, treeview, liststore)
1133 hbox.pack_start(button,False)
1136 vbox.pack_start(scroll,True)
1137 vbox.pack_start(hbox, False)
1140 def update_status_bar(self):
1141 interface = self.wallet.interface
1142 if self.funds_error:
1143 text = "Not enough funds"
1144 elif interface and interface.is_connected:
1145 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1146 if self.wallet.blocks == -1:
1147 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1148 text = "Connecting..."
1149 elif self.wallet.blocks == 0:
1150 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1151 text = "Server not ready"
1152 elif not self.wallet.up_to_date:
1153 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1154 text = "Synchronizing..."
1156 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1157 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1158 c, u = self.wallet.get_balance()
1159 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1160 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1162 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1163 self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(self.wallet.server, self.wallet.blocks))
1164 text = "Not connected"
1166 self.status_bar.pop(self.context_id)
1167 self.status_bar.push(self.context_id, text)
1169 if self.wallet.was_updated and self.wallet.up_to_date:
1170 self.update_history_tab()
1171 self.update_receiving_tab()
1172 # addressbook too...
1173 self.info.set_text( self.wallet.banner )
1174 self.wallet.was_updated = False
1177 def update_receiving_tab(self):
1178 self.recv_list.clear()
1179 for address in self.wallet.all_addresses():
1180 if self.wallet.is_change(address):continue
1181 label = self.wallet.labels.get(address)
1183 h = self.wallet.history.get(address,[])
1185 if not item['is_input'] : n=n+1
1186 tx = "None" if n==0 else "%d"%n
1187 self.recv_list.append((address, label, tx ))
1189 def update_sending_tab(self):
1190 # detect addresses that are not mine in history, add them here...
1191 self.addressbook_list.clear()
1192 for alias, v in self.wallet.aliases.items():
1194 label = self.wallet.labels.get(alias)
1195 self.addressbook_list.append((alias, label, '-'))
1197 for address in self.wallet.addressbook:
1198 label = self.wallet.labels.get(address)
1200 for item in self.wallet.tx_history.values():
1201 if address in item['outputs'] : n=n+1
1202 tx = "None" if n==0 else "%d"%n
1203 self.addressbook_list.append((address, label, tx))
1205 def update_history_tab(self):
1206 cursor = self.history_treeview.get_cursor()[0]
1207 self.history_list.clear()
1209 for tx in self.wallet.get_tx_history():
1210 tx_hash = tx['tx_hash']
1212 conf = self.wallet.blocks - tx['height'] + 1
1213 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
1214 conf_icon = gtk.STOCK_APPLY
1217 time_str = 'pending'
1218 conf_icon = gtk.STOCK_EXECUTE
1221 label = self.wallet.labels.get(tx_hash)
1222 is_default_label = (label == '') or (label is None)
1223 if is_default_label: label = tx['default_label']
1224 tooltip = tx_hash + "\n%d confirmations"%conf
1226 # tx = self.wallet.tx_history.get(tx_hash)
1227 details = "Transaction Details:\n\n" \
1228 + "Transaction ID:\n" + tx_hash + "\n\n" \
1229 + "Status: %d confirmations\n\n"%conf \
1230 + "Date: %s\n\n"%time_str \
1231 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
1232 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
1233 r = self.wallet.receipts.get(tx_hash)
1235 details += "\n_______________________________________" \
1236 + '\n\nSigned URI: ' + r[2] \
1237 + "\n\nSigned by: " + r[0] \
1238 + '\n\nSignature: ' + r[1]
1241 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1242 format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1243 if cursor: self.history_treeview.set_cursor( cursor )
1247 def newaddress_dialog(self, w):
1249 title = "New Contact"
1250 dialog = gtk.Dialog(title, parent=self.window,
1251 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1252 buttons= ("cancel", 0, "ok",1) )
1256 label_label = gtk.Label('Label:')
1257 label_label.set_size_request(120,10)
1259 label.pack_start(label_label)
1260 label_entry = gtk.Entry()
1262 label.pack_start(label_entry)
1264 dialog.vbox.pack_start(label, False, True, 5)
1266 address = gtk.HBox()
1267 address_label = gtk.Label('Address:')
1268 address_label.set_size_request(120,10)
1269 address_label.show()
1270 address.pack_start(address_label)
1271 address_entry = gtk.Entry()
1272 address_entry.show()
1273 address.pack_start(address_entry)
1275 dialog.vbox.pack_start(address, False, True, 5)
1277 result = dialog.run()
1278 address = address_entry.get_text()
1279 label = label_entry.get_text()
1283 if self.wallet.is_valid(address):
1284 self.wallet.addressbook.append(address)
1285 if label: self.wallet.labels[address] = label
1287 self.update_sending_tab()
1289 errorDialog = gtk.MessageDialog(
1291 flags=gtk.DIALOG_MODAL,
1292 buttons= gtk.BUTTONS_CLOSE,
1293 message_format = "Invalid address")
1296 errorDialog.destroy()
1300 class ElectrumGui():
1302 def __init__(self, wallet, config):
1303 self.wallet = wallet
1304 self.config = config
1306 def main(self, url=None):
1307 ew = ElectrumWindow(self.wallet, self.config)
1308 if url: ew.set_url(url)
1311 def restore_or_create(self):
1312 return restore_create_dialog(self.wallet)
1314 def server_list_changed(self):