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
27 import pyqrnative, mnemonic
29 gtk.gdk.threads_init()
32 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
34 from wallet import format_satoshis
35 from interface import DEFAULT_SERVERS
37 def numbify(entry, is_int = False):
38 text = entry.get_text().strip()
40 if not is_int: chars +='.'
41 s = ''.join([i for i in text if i in chars])
46 s = s[:p] + '.' + s[p:p+8]
48 amount = int( Decimal(s) * 100000000 )
62 def show_seed_dialog(wallet, password, parent):
64 show_message("No seed")
67 seed = wallet.pw_decode( wallet.seed, password)
69 show_message("Incorrect password")
71 dialog = gtk.MessageDialog(
73 flags = gtk.DIALOG_MODAL,
74 buttons = gtk.BUTTONS_OK,
75 message_format = "Your wallet generation seed is:\n\n" + seed \
76 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
77 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
78 dialog.set_title("Seed")
83 def restore_create_dialog(wallet):
85 # ask if the user wants to create a new wallet, or recover from a seed.
86 # if he wants to recover, and nothing is found, do not create wallet
87 dialog = gtk.Dialog("electrum", parent=None,
88 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
89 buttons= ("create", 0, "restore",1, "cancel",2) )
91 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
93 dialog.vbox.pack_start(label)
102 # ask for the server.
103 if not run_network_dialog( wallet, parent=None ): return False
107 wallet.new_seed(None)
109 wallet.init_mpk( wallet.seed )
110 wallet.up_to_date_event.clear()
113 # run a dialog indicating the seed, ask the user to remember it
114 show_seed_dialog(wallet, None, None)
117 change_password_dialog(wallet, None, None)
119 # ask for seed and gap.
120 run_recovery_dialog( wallet )
122 dialog = gtk.MessageDialog(
124 flags = gtk.DIALOG_MODAL,
125 buttons = gtk.BUTTONS_CANCEL,
126 message_format = "Please wait..." )
129 def recover_thread( wallet, dialog ):
130 wallet.init_mpk( wallet.seed ) # not encrypted at this point
131 wallet.up_to_date_event.clear()
134 if wallet.is_found():
135 # history and addressbook
136 wallet.update_tx_history()
137 wallet.fill_addressbook()
138 print "recovery successful"
140 gobject.idle_add( dialog.destroy )
142 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
145 if r==gtk.RESPONSE_CANCEL: return False
146 if not wallet.is_found:
147 show_message("No transactions found for this seed")
153 def run_recovery_dialog(wallet):
154 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
155 dialog = gtk.MessageDialog(
157 flags = gtk.DIALOG_MODAL,
158 buttons = gtk.BUTTONS_OK_CANCEL,
159 message_format = message)
162 dialog.set_default_response(gtk.RESPONSE_OK)
164 # ask seed, server and gap in the same dialog
165 seed_box = gtk.HBox()
166 seed_label = gtk.Label('Seed or mnemonic:')
167 seed_label.set_size_request(150,-1)
168 seed_box.pack_start(seed_label, False, False, 10)
170 seed_entry = gtk.Entry()
172 seed_entry.set_size_request(450,-1)
173 seed_box.pack_start(seed_entry, False, False, 10)
174 add_help_button(seed_box, '.')
176 vbox.pack_start(seed_box, False, False, 5)
179 gap_label = gtk.Label('Gap limit:')
180 gap_label.set_size_request(150,10)
182 gap.pack_start(gap_label,False, False, 10)
183 gap_entry = gtk.Entry()
184 gap_entry.set_text("%d"%wallet.gap_limit)
185 gap_entry.connect('changed', numbify, True)
187 gap.pack_start(gap_entry,False,False, 10)
188 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.')
190 vbox.pack_start(gap, False,False, 5)
194 gap = gap_entry.get_text()
195 seed = seed_entry.get_text()
198 if r==gtk.RESPONSE_CANCEL:
203 show_message("error")
209 print "not hex, trying decode"
210 seed = mnemonic.mn_decode( seed.split(' ') )
212 show_message("no seed")
216 wallet.gap_limit = gap
221 def run_settings_dialog(wallet, parent):
223 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
225 dialog = gtk.MessageDialog(
227 flags = gtk.DIALOG_MODAL,
228 buttons = gtk.BUTTONS_OK_CANCEL,
229 message_format = message)
232 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
234 dialog.set_image(image)
235 dialog.set_title("Settings")
238 dialog.set_default_response(gtk.RESPONSE_OK)
241 fee_entry = gtk.Entry()
242 fee_label = gtk.Label('Transaction fee:')
243 fee_label.set_size_request(150,10)
245 fee.pack_start(fee_label,False, False, 10)
246 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
247 fee_entry.connect('changed', numbify, False)
249 fee.pack_start(fee_entry,False,False, 10)
250 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
252 vbox.pack_start(fee, False,False, 5)
255 nz_entry = gtk.Entry()
256 nz_label = gtk.Label('Display zeros:')
257 nz_label.set_size_request(150,10)
259 nz.pack_start(nz_label,False, False, 10)
260 nz_entry.set_text( str( wallet.num_zeros ))
261 nz_entry.connect('changed', numbify, True)
263 nz.pack_start(nz_entry,False,False, 10)
264 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'")
266 vbox.pack_start(nz, False,False, 5)
270 fee = fee_entry.get_text()
271 nz = nz_entry.get_text()
274 if r==gtk.RESPONSE_CANCEL:
278 fee = int( 100000000 * Decimal(fee) )
280 show_message("error")
282 if wallet.fee != fee:
290 show_message("error")
292 if wallet.num_zeros != nz:
293 wallet.num_zeros = nz
299 def run_network_dialog( wallet, parent ):
301 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
302 interface = wallet.interface
304 if interface.is_connected:
305 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
307 status = "Not connected"
308 server = wallet.server
311 status = "Please choose a server."
312 server = random.choice( DEFAULT_SERVERS )
314 if not wallet.interface.servers:
316 for x in DEFAULT_SERVERS:
317 h,port,protocol = x.split(':')
318 servers_list.append( (h,[(protocol,port)] ) )
320 servers_list = wallet.interface.servers
323 for item in servers_list:
327 protocol, port = item2
331 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
332 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
333 dialog.set_title("Server")
334 dialog.set_image(image)
338 host_box = gtk.HBox()
339 host_label = gtk.Label('Connect to:')
340 host_label.set_size_request(100,-1)
342 host_box.pack_start(host_label, False, False, 10)
343 host_entry = gtk.Entry()
344 host_entry.set_size_request(200,-1)
345 host_entry.set_text(server)
347 host_box.pack_start(host_entry, False, False, 10)
348 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)')
352 p_box = gtk.HBox(False, 10)
355 p_label = gtk.Label('Protocol:')
356 p_label.set_size_request(100,-1)
358 p_box.pack_start(p_label, False, False, 10)
360 radio1 = gtk.RadioButton(None, "tcp")
361 p_box.pack_start(radio1, True, True, 0)
363 radio2 = gtk.RadioButton(radio1, "http")
364 p_box.pack_start(radio2, True, True, 0)
368 return unicode(host_entry.get_text()).split(':')
370 def set_button(protocol):
373 elif protocol == 'h':
376 def set_protocol(protocol):
377 host = current_line()[0]
379 if protocol not in pp.keys():
380 protocol = pp.keys()[0]
383 host_entry.set_text( host + ':' + port + ':' + protocol)
385 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
386 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
388 server_list = gtk.ListStore(str)
389 for host in plist.keys():
390 server_list.append([host])
392 treeview = gtk.TreeView(model=server_list)
395 if wallet.interface.servers:
396 label = 'Active Servers'
398 label = 'Default Servers'
400 tvcolumn = gtk.TreeViewColumn(label)
401 treeview.append_column(tvcolumn)
402 cell = gtk.CellRendererText()
403 tvcolumn.pack_start(cell, False)
404 tvcolumn.add_attribute(cell, 'text', 0)
406 scroll = gtk.ScrolledWindow()
407 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
411 vbox.pack_start(host_box, False,False, 5)
412 vbox.pack_start(p_box, True, True, 0)
413 vbox.pack_start(scroll)
415 def my_treeview_cb(treeview):
416 path, view_column = treeview.get_cursor()
417 host = server_list.get_value( server_list.get_iter(path), 0)
423 protocol = pp.keys()[0]
425 host_entry.set_text( host + ':' + port + ':' + protocol)
428 treeview.connect('cursor-changed', my_treeview_cb)
432 server = host_entry.get_text()
435 if r==gtk.RESPONSE_CANCEL:
439 wallet.set_server(server)
441 show_message("error:" + server)
450 def show_message(message, parent=None):
451 dialog = gtk.MessageDialog(
453 flags = gtk.DIALOG_MODAL,
454 buttons = gtk.BUTTONS_CLOSE,
455 message_format = message )
460 def password_line(label):
461 password = gtk.HBox()
462 password_label = gtk.Label(label)
463 password_label.set_size_request(120,10)
464 password_label.show()
465 password.pack_start(password_label,False, False, 10)
466 password_entry = gtk.Entry()
467 password_entry.set_size_request(300,-1)
468 password_entry.set_visibility(False)
469 password_entry.show()
470 password.pack_start(password_entry,False,False, 10)
472 return password, password_entry
474 def password_dialog(parent):
475 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
476 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
477 dialog.get_image().set_visible(False)
478 current_pw, current_pw_entry = password_line('Password:')
479 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
480 dialog.vbox.pack_start(current_pw, False, True, 0)
482 result = dialog.run()
483 pw = current_pw_entry.get_text()
485 if result != gtk.RESPONSE_CANCEL: return pw
487 def change_password_dialog(wallet, parent, icon):
489 show_message("No seed")
493 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'
495 msg = "Please choose a password to encrypt your wallet keys"
497 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
498 dialog.set_title("Change password")
500 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
502 dialog.set_image(image)
504 if wallet.use_encryption:
505 current_pw, current_pw_entry = password_line('Current password:')
506 dialog.vbox.pack_start(current_pw, False, True, 0)
508 password, password_entry = password_line('New password:')
509 dialog.vbox.pack_start(password, False, True, 5)
510 password2, password2_entry = password_line('Confirm password:')
511 dialog.vbox.pack_start(password2, False, True, 5)
514 result = dialog.run()
515 password = current_pw_entry.get_text() if wallet.use_encryption else None
516 new_password = password_entry.get_text()
517 new_password2 = password2_entry.get_text()
519 if result == gtk.RESPONSE_CANCEL:
523 seed = wallet.pw_decode( wallet.seed, password)
525 show_message("Incorrect password")
528 if new_password != new_password2:
529 show_message("passwords do not match")
532 wallet.update_password(seed, password, new_password)
535 if wallet.use_encryption:
536 icon.set_tooltip_text('wallet is encrypted')
538 icon.set_tooltip_text('wallet is unencrypted')
541 def add_help_button(hbox, message):
542 button = gtk.Button('?')
543 button.connect("clicked", lambda x: show_message(message))
545 hbox.pack_start(button,False, False)
548 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
550 gobject.type_register(MyWindow)
551 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
552 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
555 class ElectrumWindow:
557 def show_message(self, msg):
558 show_message(msg, self.window)
560 def __init__(self, wallet):
562 self.funds_error = False # True if not enough funds
564 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
565 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
566 if not self.wallet.seed: title += ' [seedless]'
567 self.window.set_title(title)
568 self.window.connect("destroy", gtk.main_quit)
569 self.window.set_border_width(0)
570 self.window.connect('mykeypress', gtk.main_quit)
571 self.window.set_default_size(720, 350)
575 self.notebook = gtk.Notebook()
576 self.create_history_tab()
578 self.create_send_tab()
579 self.create_recv_tab()
580 self.create_book_tab()
581 self.create_about_tab()
583 vbox.pack_start(self.notebook, True, True, 2)
585 self.status_bar = gtk.Statusbar()
586 vbox.pack_start(self.status_bar, False, False, 0)
588 self.status_image = gtk.Image()
589 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
590 self.status_image.set_alignment(True, 0.5 )
591 self.status_image.show()
593 self.network_button = gtk.Button()
594 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
595 self.network_button.add(self.status_image)
596 self.network_button.set_relief(gtk.RELIEF_NONE)
597 self.network_button.show()
598 self.status_bar.pack_end(self.network_button, False, False)
601 def seedb(w, wallet):
602 if wallet.use_encryption:
603 password = password_dialog(self.window)
604 if not password: return
605 else: password = None
606 show_seed_dialog(wallet, password, self.window)
607 button = gtk.Button('S')
608 button.connect("clicked", seedb, wallet )
609 button.set_relief(gtk.RELIEF_NONE)
611 self.status_bar.pack_end(button,False, False)
613 settings_icon = gtk.Image()
614 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
615 settings_icon.set_alignment(0.5, 0.5)
616 settings_icon.set_size_request(16,16 )
619 prefs_button = gtk.Button()
620 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
621 prefs_button.add(settings_icon)
622 prefs_button.set_tooltip_text("Settings")
623 prefs_button.set_relief(gtk.RELIEF_NONE)
625 self.status_bar.pack_end(prefs_button,False,False)
627 pw_icon = gtk.Image()
628 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
629 pw_icon.set_alignment(0.5, 0.5)
630 pw_icon.set_size_request(16,16 )
634 password_button = gtk.Button()
635 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
636 password_button.add(pw_icon)
637 password_button.set_relief(gtk.RELIEF_NONE)
638 password_button.show()
639 self.status_bar.pack_end(password_button,False,False)
641 self.window.add(vbox)
642 self.window.show_all()
645 self.context_id = self.status_bar.get_context_id("statusbar")
646 self.update_status_bar()
648 def update_status_bar_thread():
650 gobject.idle_add( self.update_status_bar )
654 def check_recipient_thread():
658 if self.payto_entry.is_focus():
660 r = self.payto_entry.get_text()
664 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
666 to_address = self.wallet.get_alias(r, interactive=False)
670 s = r + ' <' + to_address + '>'
671 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
674 thread.start_new_thread(update_status_bar_thread, ())
676 thread.start_new_thread(check_recipient_thread, ())
677 self.notebook.set_current_page(0)
680 def add_tab(self, page, name):
681 tab_label = gtk.Label(name)
683 self.notebook.append_page(page, tab_label)
686 def create_send_tab(self):
688 page = vbox = gtk.VBox()
692 payto_label = gtk.Label('Pay to:')
693 payto_label.set_size_request(100,-1)
694 payto.pack_start(payto_label, False)
695 payto_entry = gtk.Entry()
696 payto_entry.set_size_request(450, 26)
697 payto.pack_start(payto_entry, False)
698 vbox.pack_start(payto, False, False, 5)
701 message_label = gtk.Label('Description:')
702 message_label.set_size_request(100,-1)
703 message.pack_start(message_label, False)
704 message_entry = gtk.Entry()
705 message_entry.set_size_request(450, 26)
706 message.pack_start(message_entry, False)
707 vbox.pack_start(message, False, False, 5)
709 amount_box = gtk.HBox()
710 amount_label = gtk.Label('Amount:')
711 amount_label.set_size_request(100,-1)
712 amount_box.pack_start(amount_label, False)
713 amount_entry = gtk.Entry()
714 amount_entry.set_size_request(120, -1)
715 amount_box.pack_start(amount_entry, False)
716 vbox.pack_start(amount_box, False, False, 5)
718 self.fee_box = fee_box = gtk.HBox()
719 fee_label = gtk.Label('Fee:')
720 fee_label.set_size_request(100,-1)
721 fee_box.pack_start(fee_label, False)
722 fee_entry = gtk.Entry()
723 fee_entry.set_size_request(60, 26)
724 fee_box.pack_start(fee_entry, False)
725 vbox.pack_start(fee_box, False, False, 5)
728 empty_label = gtk.Label('')
729 empty_label.set_size_request(100,-1)
730 end_box.pack_start(empty_label, False)
731 send_button = gtk.Button("Send")
733 end_box.pack_start(send_button, False, False, 0)
734 clear_button = gtk.Button("Clear")
736 end_box.pack_start(clear_button, False, False, 15)
737 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
738 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
740 vbox.pack_start(end_box, False, False, 5)
742 # display this line only if there is a signature
743 payto_sig = gtk.HBox()
744 payto_sig_id = gtk.Label('')
745 payto_sig.pack_start(payto_sig_id, False)
746 vbox.pack_start(payto_sig, True, True, 5)
749 self.user_fee = False
751 def entry_changed( entry, is_fee ):
752 self.funds_error = False
753 amount = numbify(amount_entry)
754 fee = numbify(fee_entry)
755 if not is_fee: fee = None
758 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
760 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
763 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
764 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
765 send_button.set_sensitive(True)
767 send_button.set_sensitive(False)
768 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
769 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
770 self.funds_error = True
772 amount_entry.connect('changed', entry_changed, False)
773 fee_entry.connect('changed', entry_changed, True)
775 self.payto_entry = payto_entry
776 self.payto_fee_entry = fee_entry
777 self.payto_sig_id = payto_sig_id
778 self.payto_sig = payto_sig
779 self.amount_entry = amount_entry
780 self.message_entry = message_entry
781 self.add_tab(page, 'Send')
783 def set_frozen(self,entry,frozen):
785 entry.set_editable(False)
786 entry.set_has_frame(False)
787 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
789 entry.set_editable(True)
790 entry.set_has_frame(True)
791 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
793 def set_url(self, url):
794 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
795 self.notebook.set_current_page(1)
796 self.payto_entry.set_text(payto)
797 self.message_entry.set_text(message)
798 self.amount_entry.set_text(amount)
800 self.set_frozen(self.payto_entry,True)
801 self.set_frozen(self.amount_entry,True)
802 self.set_frozen(self.message_entry,True)
803 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
805 self.payto_sig.set_visible(False)
807 def create_about_tab(self):
812 tv.set_editable(False)
813 tv.set_cursor_visible(False)
814 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
816 self.info = tv.get_buffer()
817 self.add_tab(page, 'Wall')
819 def do_clear(self, w, data):
820 self.payto_sig.set_visible(False)
821 self.payto_fee_entry.set_text('')
822 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
823 self.set_frozen(entry,False)
826 def question(self,msg):
827 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
829 result = dialog.run()
831 return result == gtk.RESPONSE_OK
833 def do_send(self, w, data):
834 payto_entry, label_entry, amount_entry, fee_entry = data
835 label = label_entry.get_text()
836 r = payto_entry.get_text()
839 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
840 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
843 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
847 self.update_sending_tab()
850 to_address = m2.group(5)
854 if not self.wallet.is_valid(to_address):
855 self.show_message( "invalid bitcoin address:\n"+to_address)
859 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
861 self.show_message( "invalid amount")
864 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
866 self.show_message( "invalid fee")
869 if self.wallet.use_encryption:
870 password = password_dialog(self.window)
877 tx = self.wallet.mktx( to_address, amount, label, password, fee )
878 except BaseException, e:
879 self.show_message(str(e))
882 status, msg = self.wallet.sendtx( tx )
884 self.show_message( "payment sent.\n" + msg )
885 payto_entry.set_text("")
886 label_entry.set_text("")
887 amount_entry.set_text("")
888 fee_entry.set_text("")
890 self.update_sending_tab()
892 self.show_message( msg )
895 def treeview_button_press(self, treeview, event):
896 if event.type == gtk.gdk._2BUTTON_PRESS:
897 c = treeview.get_cursor()[0]
898 if treeview == self.history_treeview:
899 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
900 self.show_message(tx_details)
901 elif treeview == self.contacts_treeview:
902 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
903 a = self.wallet.aliases.get(m)
905 if a[0] in self.wallet.authorities.keys():
906 s = self.wallet.authorities.get(a[0])
909 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
910 self.show_message(msg)
913 def treeview_key_press(self, treeview, event):
914 c = treeview.get_cursor()[0]
915 if event.keyval == gtk.keysyms.Up:
917 treeview.parent.grab_focus()
918 treeview.set_cursor((0,))
919 elif event.keyval == gtk.keysyms.Return:
920 if treeview == self.history_treeview:
921 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
922 self.show_message(tx_details)
923 elif treeview == self.contacts_treeview:
924 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
925 a = self.wallet.aliases.get(m)
927 if a[0] in self.wallet.authorities.keys():
928 s = self.wallet.authorities.get(a[0])
931 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
932 self.show_message(msg)
936 def create_history_tab(self):
938 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
939 treeview = gtk.TreeView(model=self.history_list)
940 self.history_treeview = treeview
941 treeview.set_tooltip_column(7)
943 treeview.connect('key-press-event', self.treeview_key_press)
944 treeview.connect('button-press-event', self.treeview_button_press)
946 tvcolumn = gtk.TreeViewColumn('')
947 treeview.append_column(tvcolumn)
948 cell = gtk.CellRendererPixbuf()
949 tvcolumn.pack_start(cell, False)
950 tvcolumn.set_attributes(cell, stock_id=1)
952 tvcolumn = gtk.TreeViewColumn('Date')
953 treeview.append_column(tvcolumn)
954 cell = gtk.CellRendererText()
955 tvcolumn.pack_start(cell, False)
956 tvcolumn.add_attribute(cell, 'text', 2)
958 tvcolumn = gtk.TreeViewColumn('Description')
959 treeview.append_column(tvcolumn)
960 cell = gtk.CellRendererText()
961 cell.set_property('foreground', 'grey')
962 cell.set_property('family', MONOSPACE_FONT)
963 cell.set_property('editable', True)
964 def edited_cb(cell, path, new_text, h_list):
965 tx = h_list.get_value( h_list.get_iter(path), 0)
966 self.wallet.labels[tx] = new_text
968 self.update_history_tab()
969 cell.connect('edited', edited_cb, self.history_list)
970 def editing_started(cell, entry, path, h_list):
971 tx = h_list.get_value( h_list.get_iter(path), 0)
972 if not self.wallet.labels.get(tx): entry.set_text('')
973 cell.connect('editing-started', editing_started, self.history_list)
974 tvcolumn.set_expand(True)
975 tvcolumn.pack_start(cell, True)
976 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
978 tvcolumn = gtk.TreeViewColumn('Amount')
979 treeview.append_column(tvcolumn)
980 cell = gtk.CellRendererText()
981 cell.set_alignment(1, 0.5)
982 cell.set_property('family', MONOSPACE_FONT)
983 tvcolumn.pack_start(cell, False)
984 tvcolumn.add_attribute(cell, 'text', 5)
986 tvcolumn = gtk.TreeViewColumn('Balance')
987 treeview.append_column(tvcolumn)
988 cell = gtk.CellRendererText()
989 cell.set_alignment(1, 0.5)
990 cell.set_property('family', MONOSPACE_FONT)
991 tvcolumn.pack_start(cell, False)
992 tvcolumn.add_attribute(cell, 'text', 6)
994 tvcolumn = gtk.TreeViewColumn('Tooltip')
995 treeview.append_column(tvcolumn)
996 cell = gtk.CellRendererText()
997 tvcolumn.pack_start(cell, False)
998 tvcolumn.add_attribute(cell, 'text', 7)
999 tvcolumn.set_visible(False)
1001 scroll = gtk.ScrolledWindow()
1002 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1003 scroll.add(treeview)
1005 self.add_tab(scroll, 'History')
1006 self.update_history_tab()
1009 def create_recv_tab(self):
1010 self.recv_list = gtk.ListStore(str, str, str)
1011 self.add_tab( self.make_address_list(True), 'Receive')
1012 self.update_receiving_tab()
1014 def create_book_tab(self):
1015 self.addressbook_list = gtk.ListStore(str, str, str)
1016 self.add_tab( self.make_address_list(False), 'Contacts')
1017 self.update_sending_tab()
1019 def make_address_list(self, is_recv):
1020 liststore = self.recv_list if is_recv else self.addressbook_list
1021 treeview = gtk.TreeView(model= liststore)
1022 treeview.connect('key-press-event', self.treeview_key_press)
1023 treeview.connect('button-press-event', self.treeview_button_press)
1026 self.contacts_treeview = treeview
1028 tvcolumn = gtk.TreeViewColumn('Address')
1029 treeview.append_column(tvcolumn)
1030 cell = gtk.CellRendererText()
1031 cell.set_property('family', MONOSPACE_FONT)
1032 tvcolumn.pack_start(cell, True)
1033 tvcolumn.add_attribute(cell, 'text', 0)
1035 tvcolumn = gtk.TreeViewColumn('Label')
1036 tvcolumn.set_expand(True)
1037 treeview.append_column(tvcolumn)
1038 cell = gtk.CellRendererText()
1039 cell.set_property('editable', True)
1040 def edited_cb2(cell, path, new_text, liststore):
1041 address = liststore.get_value( liststore.get_iter(path), 0)
1042 self.wallet.labels[address] = new_text
1044 self.wallet.update_tx_labels()
1045 self.update_receiving_tab()
1046 self.update_sending_tab()
1047 self.update_history_tab()
1048 cell.connect('edited', edited_cb2, liststore)
1049 tvcolumn.pack_start(cell, True)
1050 tvcolumn.add_attribute(cell, 'text', 1)
1052 tvcolumn = gtk.TreeViewColumn('Tx')
1053 treeview.append_column(tvcolumn)
1054 cell = gtk.CellRendererText()
1055 tvcolumn.pack_start(cell, True)
1056 tvcolumn.add_attribute(cell, 'text', 2)
1058 scroll = gtk.ScrolledWindow()
1059 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1060 scroll.add(treeview)
1064 button = gtk.Button("New")
1065 button.connect("clicked", self.newaddress_dialog)
1067 hbox.pack_start(button,False)
1069 def showqrcode(w, treeview, liststore):
1070 path, col = treeview.get_cursor()
1072 address = liststore.get_value(liststore.get_iter(path), 0)
1073 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1077 size = qr.getModuleCount()*boxsize
1078 def area_expose_cb(area, event):
1079 style = area.get_style()
1080 k = qr.getModuleCount()
1083 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1084 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1085 area = gtk.DrawingArea()
1086 area.set_size_request(size, size)
1087 area.connect("expose-event", area_expose_cb)
1089 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1090 dialog.vbox.add(area)
1094 button = gtk.Button("QR")
1095 button.connect("clicked", showqrcode, treeview, liststore)
1097 hbox.pack_start(button,False)
1099 button = gtk.Button("Copy to clipboard")
1100 def copy2clipboard(w, treeview, liststore):
1102 path, col = treeview.get_cursor()
1104 address = liststore.get_value( liststore.get_iter(path), 0)
1105 if platform.system() == 'Windows':
1106 from Tkinter import Tk
1110 r.clipboard_append( address )
1113 c = gtk.clipboard_get()
1114 c.set_text( address )
1115 button.connect("clicked", copy2clipboard, treeview, liststore)
1117 hbox.pack_start(button,False)
1120 button = gtk.Button("Pay to")
1121 def payto(w, treeview, liststore):
1122 path, col = treeview.get_cursor()
1124 address = liststore.get_value( liststore.get_iter(path), 0)
1125 self.payto_entry.set_text( address )
1126 self.notebook.set_current_page(1)
1127 self.amount_entry.grab_focus()
1129 button.connect("clicked", payto, treeview, liststore)
1131 hbox.pack_start(button,False)
1134 vbox.pack_start(scroll,True)
1135 vbox.pack_start(hbox, False)
1138 def update_status_bar(self):
1139 interface = self.wallet.interface
1140 if self.funds_error:
1141 text = "Not enough funds"
1142 elif interface and interface.is_connected:
1143 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1144 if self.wallet.blocks == -1:
1145 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1146 text = "Connecting..."
1147 elif self.wallet.blocks == 0:
1148 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1149 text = "Server not ready"
1150 elif not self.wallet.up_to_date:
1151 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1152 text = "Synchronizing..."
1154 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1155 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1156 c, u = self.wallet.get_balance()
1157 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1158 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1160 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1161 self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(self.wallet.server, self.wallet.blocks))
1162 text = "Not connected"
1164 self.status_bar.pop(self.context_id)
1165 self.status_bar.push(self.context_id, text)
1167 if self.wallet.was_updated and self.wallet.up_to_date:
1168 self.update_history_tab()
1169 self.update_receiving_tab()
1170 # addressbook too...
1171 self.info.set_text( self.wallet.banner )
1172 self.wallet.was_updated = False
1175 def update_receiving_tab(self):
1176 self.recv_list.clear()
1177 for address in self.wallet.all_addresses():
1178 if self.wallet.is_change(address):continue
1179 label = self.wallet.labels.get(address)
1181 h = self.wallet.history.get(address,[])
1183 if not item['is_input'] : n=n+1
1184 tx = "None" if n==0 else "%d"%n
1185 self.recv_list.append((address, label, tx ))
1187 def update_sending_tab(self):
1188 # detect addresses that are not mine in history, add them here...
1189 self.addressbook_list.clear()
1190 for alias, v in self.wallet.aliases.items():
1192 label = self.wallet.labels.get(alias)
1193 self.addressbook_list.append((alias, label, '-'))
1195 for address in self.wallet.addressbook:
1196 label = self.wallet.labels.get(address)
1198 for item in self.wallet.tx_history.values():
1199 if address in item['outputs'] : n=n+1
1200 tx = "None" if n==0 else "%d"%n
1201 self.addressbook_list.append((address, label, tx))
1203 def update_history_tab(self):
1204 cursor = self.history_treeview.get_cursor()[0]
1205 self.history_list.clear()
1207 for tx in self.wallet.get_tx_history():
1208 tx_hash = tx['tx_hash']
1210 conf = self.wallet.blocks - tx['height'] + 1
1211 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
1212 conf_icon = gtk.STOCK_APPLY
1215 time_str = 'pending'
1216 conf_icon = gtk.STOCK_EXECUTE
1219 label = self.wallet.labels.get(tx_hash)
1220 is_default_label = (label == '') or (label is None)
1221 if is_default_label: label = tx['default_label']
1222 tooltip = tx_hash + "\n%d confirmations"%conf
1224 # tx = self.wallet.tx_history.get(tx_hash)
1225 details = "Transaction Details:\n\n" \
1226 + "Transaction ID:\n" + tx_hash + "\n\n" \
1227 + "Status: %d confirmations\n\n"%conf \
1228 + "Date: %s\n\n"%time_str \
1229 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
1230 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
1231 r = self.wallet.receipts.get(tx_hash)
1233 details += "\n_______________________________________" \
1234 + '\n\nSigned URI: ' + r[2] \
1235 + "\n\nSigned by: " + r[0] \
1236 + '\n\nSignature: ' + r[1]
1239 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1240 format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1241 if cursor: self.history_treeview.set_cursor( cursor )
1245 def newaddress_dialog(self, w):
1247 title = "New Contact"
1248 dialog = gtk.Dialog(title, parent=self.window,
1249 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1250 buttons= ("cancel", 0, "ok",1) )
1254 label_label = gtk.Label('Label:')
1255 label_label.set_size_request(120,10)
1257 label.pack_start(label_label)
1258 label_entry = gtk.Entry()
1260 label.pack_start(label_entry)
1262 dialog.vbox.pack_start(label, False, True, 5)
1264 address = gtk.HBox()
1265 address_label = gtk.Label('Address:')
1266 address_label.set_size_request(120,10)
1267 address_label.show()
1268 address.pack_start(address_label)
1269 address_entry = gtk.Entry()
1270 address_entry.show()
1271 address.pack_start(address_entry)
1273 dialog.vbox.pack_start(address, False, True, 5)
1275 result = dialog.run()
1276 address = address_entry.get_text()
1277 label = label_entry.get_text()
1281 if self.wallet.is_valid(address):
1282 self.wallet.addressbook.append(address)
1283 if label: self.wallet.labels[address] = label
1285 self.update_sending_tab()
1287 errorDialog = gtk.MessageDialog(
1289 flags=gtk.DIALOG_MODAL,
1290 buttons= gtk.BUTTONS_CLOSE,
1291 message_format = "Invalid address")
1294 errorDialog.destroy()
1298 class ElectrumGui():
1300 def __init__(self, wallet):
1301 self.wallet = wallet
1303 def main(self, url=None):
1304 ew = ElectrumWindow(self.wallet)
1305 if url: ew.set_url(url)
1308 def restore_or_create(self):
1309 return restore_create_dialog(self.wallet)