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 sys.stderr.write("Warning: Not hex, trying decode\n")
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):
563 self.funds_error = False # True if not enough funds
565 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
566 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
567 if not self.wallet.seed: title += ' [seedless]'
568 self.window.set_title(title)
569 self.window.connect("destroy", gtk.main_quit)
570 self.window.set_border_width(0)
571 self.window.connect('mykeypress', gtk.main_quit)
572 self.window.set_default_size(720, 350)
576 self.notebook = gtk.Notebook()
577 self.create_history_tab()
579 self.create_send_tab()
580 self.create_recv_tab()
581 self.create_book_tab()
582 self.create_about_tab()
584 vbox.pack_start(self.notebook, True, True, 2)
586 self.status_bar = gtk.Statusbar()
587 vbox.pack_start(self.status_bar, False, False, 0)
589 self.status_image = gtk.Image()
590 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
591 self.status_image.set_alignment(True, 0.5 )
592 self.status_image.show()
594 self.network_button = gtk.Button()
595 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
596 self.network_button.add(self.status_image)
597 self.network_button.set_relief(gtk.RELIEF_NONE)
598 self.network_button.show()
599 self.status_bar.pack_end(self.network_button, False, False)
602 def seedb(w, wallet):
603 if wallet.use_encryption:
604 password = password_dialog(self.window)
605 if not password: return
606 else: password = None
607 show_seed_dialog(wallet, password, self.window)
608 button = gtk.Button('S')
609 button.connect("clicked", seedb, wallet )
610 button.set_relief(gtk.RELIEF_NONE)
612 self.status_bar.pack_end(button,False, False)
614 settings_icon = gtk.Image()
615 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
616 settings_icon.set_alignment(0.5, 0.5)
617 settings_icon.set_size_request(16,16 )
620 prefs_button = gtk.Button()
621 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
622 prefs_button.add(settings_icon)
623 prefs_button.set_tooltip_text("Settings")
624 prefs_button.set_relief(gtk.RELIEF_NONE)
626 self.status_bar.pack_end(prefs_button,False,False)
628 pw_icon = gtk.Image()
629 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
630 pw_icon.set_alignment(0.5, 0.5)
631 pw_icon.set_size_request(16,16 )
635 password_button = gtk.Button()
636 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
637 password_button.add(pw_icon)
638 password_button.set_relief(gtk.RELIEF_NONE)
639 password_button.show()
640 self.status_bar.pack_end(password_button,False,False)
642 self.window.add(vbox)
643 self.window.show_all()
646 self.context_id = self.status_bar.get_context_id("statusbar")
647 self.update_status_bar()
649 def update_status_bar_thread():
651 gobject.idle_add( self.update_status_bar )
655 def check_recipient_thread():
659 if self.payto_entry.is_focus():
661 r = self.payto_entry.get_text()
665 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
667 to_address = self.wallet.get_alias(r, interactive=False)
671 s = r + ' <' + to_address + '>'
672 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
675 thread.start_new_thread(update_status_bar_thread, ())
677 thread.start_new_thread(check_recipient_thread, ())
678 self.notebook.set_current_page(0)
681 def add_tab(self, page, name):
682 tab_label = gtk.Label(name)
684 self.notebook.append_page(page, tab_label)
687 def create_send_tab(self):
689 page = vbox = gtk.VBox()
693 payto_label = gtk.Label('Pay to:')
694 payto_label.set_size_request(100,-1)
695 payto.pack_start(payto_label, False)
696 payto_entry = gtk.Entry()
697 payto_entry.set_size_request(450, 26)
698 payto.pack_start(payto_entry, False)
699 vbox.pack_start(payto, False, False, 5)
702 message_label = gtk.Label('Description:')
703 message_label.set_size_request(100,-1)
704 message.pack_start(message_label, False)
705 message_entry = gtk.Entry()
706 message_entry.set_size_request(450, 26)
707 message.pack_start(message_entry, False)
708 vbox.pack_start(message, False, False, 5)
710 amount_box = gtk.HBox()
711 amount_label = gtk.Label('Amount:')
712 amount_label.set_size_request(100,-1)
713 amount_box.pack_start(amount_label, False)
714 amount_entry = gtk.Entry()
715 amount_entry.set_size_request(120, -1)
716 amount_box.pack_start(amount_entry, False)
717 vbox.pack_start(amount_box, False, False, 5)
719 self.fee_box = fee_box = gtk.HBox()
720 fee_label = gtk.Label('Fee:')
721 fee_label.set_size_request(100,-1)
722 fee_box.pack_start(fee_label, False)
723 fee_entry = gtk.Entry()
724 fee_entry.set_size_request(60, 26)
725 fee_box.pack_start(fee_entry, False)
726 vbox.pack_start(fee_box, False, False, 5)
729 empty_label = gtk.Label('')
730 empty_label.set_size_request(100,-1)
731 end_box.pack_start(empty_label, False)
732 send_button = gtk.Button("Send")
734 end_box.pack_start(send_button, False, False, 0)
735 clear_button = gtk.Button("Clear")
737 end_box.pack_start(clear_button, False, False, 15)
738 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
739 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
741 vbox.pack_start(end_box, False, False, 5)
743 # display this line only if there is a signature
744 payto_sig = gtk.HBox()
745 payto_sig_id = gtk.Label('')
746 payto_sig.pack_start(payto_sig_id, False)
747 vbox.pack_start(payto_sig, True, True, 5)
750 self.user_fee = False
752 def entry_changed( entry, is_fee ):
753 self.funds_error = False
754 amount = numbify(amount_entry)
755 fee = numbify(fee_entry)
756 if not is_fee: fee = None
759 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
761 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
764 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
765 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
766 send_button.set_sensitive(True)
768 send_button.set_sensitive(False)
769 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
770 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
771 self.funds_error = True
773 amount_entry.connect('changed', entry_changed, False)
774 fee_entry.connect('changed', entry_changed, True)
776 self.payto_entry = payto_entry
777 self.payto_fee_entry = fee_entry
778 self.payto_sig_id = payto_sig_id
779 self.payto_sig = payto_sig
780 self.amount_entry = amount_entry
781 self.message_entry = message_entry
782 self.add_tab(page, 'Send')
784 def set_frozen(self,entry,frozen):
786 entry.set_editable(False)
787 entry.set_has_frame(False)
788 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
790 entry.set_editable(True)
791 entry.set_has_frame(True)
792 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
794 def set_url(self, url):
795 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
796 self.notebook.set_current_page(1)
797 self.payto_entry.set_text(payto)
798 self.message_entry.set_text(message)
799 self.amount_entry.set_text(amount)
801 self.set_frozen(self.payto_entry,True)
802 self.set_frozen(self.amount_entry,True)
803 self.set_frozen(self.message_entry,True)
804 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
806 self.payto_sig.set_visible(False)
808 def create_about_tab(self):
813 tv.set_editable(False)
814 tv.set_cursor_visible(False)
815 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
817 self.info = tv.get_buffer()
818 self.add_tab(page, 'Wall')
820 def do_clear(self, w, data):
821 self.payto_sig.set_visible(False)
822 self.payto_fee_entry.set_text('')
823 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
824 self.set_frozen(entry,False)
827 def question(self,msg):
828 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
830 result = dialog.run()
832 return result == gtk.RESPONSE_OK
834 def do_send(self, w, data):
835 payto_entry, label_entry, amount_entry, fee_entry = data
836 label = label_entry.get_text()
837 r = payto_entry.get_text()
840 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
841 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
844 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
848 self.update_sending_tab()
851 to_address = m2.group(5)
855 if not self.wallet.is_valid(to_address):
856 self.show_message( "invalid bitcoin address:\n"+to_address)
860 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
862 self.show_message( "invalid amount")
865 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
867 self.show_message( "invalid fee")
870 if self.wallet.use_encryption:
871 password = password_dialog(self.window)
878 tx = self.wallet.mktx( to_address, amount, label, password, fee )
879 except BaseException, e:
880 self.show_message(str(e))
883 status, msg = self.wallet.sendtx( tx )
885 self.show_message( "payment sent.\n" + msg )
886 payto_entry.set_text("")
887 label_entry.set_text("")
888 amount_entry.set_text("")
889 fee_entry.set_text("")
891 self.update_sending_tab()
893 self.show_message( msg )
896 def treeview_button_press(self, treeview, event):
897 if event.type == gtk.gdk._2BUTTON_PRESS:
898 c = treeview.get_cursor()[0]
899 if treeview == self.history_treeview:
900 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
901 self.show_message(tx_details)
902 elif treeview == self.contacts_treeview:
903 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
904 a = self.wallet.aliases.get(m)
906 if a[0] in self.wallet.authorities.keys():
907 s = self.wallet.authorities.get(a[0])
910 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
911 self.show_message(msg)
914 def treeview_key_press(self, treeview, event):
915 c = treeview.get_cursor()[0]
916 if event.keyval == gtk.keysyms.Up:
918 treeview.parent.grab_focus()
919 treeview.set_cursor((0,))
920 elif event.keyval == gtk.keysyms.Return:
921 if treeview == self.history_treeview:
922 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
923 self.show_message(tx_details)
924 elif treeview == self.contacts_treeview:
925 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
926 a = self.wallet.aliases.get(m)
928 if a[0] in self.wallet.authorities.keys():
929 s = self.wallet.authorities.get(a[0])
932 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
933 self.show_message(msg)
937 def create_history_tab(self):
939 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
940 treeview = gtk.TreeView(model=self.history_list)
941 self.history_treeview = treeview
942 treeview.set_tooltip_column(7)
944 treeview.connect('key-press-event', self.treeview_key_press)
945 treeview.connect('button-press-event', self.treeview_button_press)
947 tvcolumn = gtk.TreeViewColumn('')
948 treeview.append_column(tvcolumn)
949 cell = gtk.CellRendererPixbuf()
950 tvcolumn.pack_start(cell, False)
951 tvcolumn.set_attributes(cell, stock_id=1)
953 tvcolumn = gtk.TreeViewColumn('Date')
954 treeview.append_column(tvcolumn)
955 cell = gtk.CellRendererText()
956 tvcolumn.pack_start(cell, False)
957 tvcolumn.add_attribute(cell, 'text', 2)
959 tvcolumn = gtk.TreeViewColumn('Description')
960 treeview.append_column(tvcolumn)
961 cell = gtk.CellRendererText()
962 cell.set_property('foreground', 'grey')
963 cell.set_property('family', MONOSPACE_FONT)
964 cell.set_property('editable', True)
965 def edited_cb(cell, path, new_text, h_list):
966 tx = h_list.get_value( h_list.get_iter(path), 0)
967 self.wallet.labels[tx] = new_text
969 self.update_history_tab()
970 cell.connect('edited', edited_cb, self.history_list)
971 def editing_started(cell, entry, path, h_list):
972 tx = h_list.get_value( h_list.get_iter(path), 0)
973 if not self.wallet.labels.get(tx): entry.set_text('')
974 cell.connect('editing-started', editing_started, self.history_list)
975 tvcolumn.set_expand(True)
976 tvcolumn.pack_start(cell, True)
977 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
979 tvcolumn = gtk.TreeViewColumn('Amount')
980 treeview.append_column(tvcolumn)
981 cell = gtk.CellRendererText()
982 cell.set_alignment(1, 0.5)
983 cell.set_property('family', MONOSPACE_FONT)
984 tvcolumn.pack_start(cell, False)
985 tvcolumn.add_attribute(cell, 'text', 5)
987 tvcolumn = gtk.TreeViewColumn('Balance')
988 treeview.append_column(tvcolumn)
989 cell = gtk.CellRendererText()
990 cell.set_alignment(1, 0.5)
991 cell.set_property('family', MONOSPACE_FONT)
992 tvcolumn.pack_start(cell, False)
993 tvcolumn.add_attribute(cell, 'text', 6)
995 tvcolumn = gtk.TreeViewColumn('Tooltip')
996 treeview.append_column(tvcolumn)
997 cell = gtk.CellRendererText()
998 tvcolumn.pack_start(cell, False)
999 tvcolumn.add_attribute(cell, 'text', 7)
1000 tvcolumn.set_visible(False)
1002 scroll = gtk.ScrolledWindow()
1003 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1004 scroll.add(treeview)
1006 self.add_tab(scroll, 'History')
1007 self.update_history_tab()
1010 def create_recv_tab(self):
1011 self.recv_list = gtk.ListStore(str, str, str)
1012 self.add_tab( self.make_address_list(True), 'Receive')
1013 self.update_receiving_tab()
1015 def create_book_tab(self):
1016 self.addressbook_list = gtk.ListStore(str, str, str)
1017 self.add_tab( self.make_address_list(False), 'Contacts')
1018 self.update_sending_tab()
1020 def make_address_list(self, is_recv):
1021 liststore = self.recv_list if is_recv else self.addressbook_list
1022 treeview = gtk.TreeView(model= liststore)
1023 treeview.connect('key-press-event', self.treeview_key_press)
1024 treeview.connect('button-press-event', self.treeview_button_press)
1027 self.contacts_treeview = treeview
1029 tvcolumn = gtk.TreeViewColumn('Address')
1030 treeview.append_column(tvcolumn)
1031 cell = gtk.CellRendererText()
1032 cell.set_property('family', MONOSPACE_FONT)
1033 tvcolumn.pack_start(cell, True)
1034 tvcolumn.add_attribute(cell, 'text', 0)
1036 tvcolumn = gtk.TreeViewColumn('Label')
1037 tvcolumn.set_expand(True)
1038 treeview.append_column(tvcolumn)
1039 cell = gtk.CellRendererText()
1040 cell.set_property('editable', True)
1041 def edited_cb2(cell, path, new_text, liststore):
1042 address = liststore.get_value( liststore.get_iter(path), 0)
1043 self.wallet.labels[address] = new_text
1045 self.wallet.update_tx_labels()
1046 self.update_receiving_tab()
1047 self.update_sending_tab()
1048 self.update_history_tab()
1049 cell.connect('edited', edited_cb2, liststore)
1050 tvcolumn.pack_start(cell, True)
1051 tvcolumn.add_attribute(cell, 'text', 1)
1053 tvcolumn = gtk.TreeViewColumn('Tx')
1054 treeview.append_column(tvcolumn)
1055 cell = gtk.CellRendererText()
1056 tvcolumn.pack_start(cell, True)
1057 tvcolumn.add_attribute(cell, 'text', 2)
1059 scroll = gtk.ScrolledWindow()
1060 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1061 scroll.add(treeview)
1065 button = gtk.Button("New")
1066 button.connect("clicked", self.newaddress_dialog)
1068 hbox.pack_start(button,False)
1070 def showqrcode(w, treeview, liststore):
1071 path, col = treeview.get_cursor()
1073 address = liststore.get_value(liststore.get_iter(path), 0)
1074 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1078 size = qr.getModuleCount()*boxsize
1079 def area_expose_cb(area, event):
1080 style = area.get_style()
1081 k = qr.getModuleCount()
1084 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1085 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1086 area = gtk.DrawingArea()
1087 area.set_size_request(size, size)
1088 area.connect("expose-event", area_expose_cb)
1090 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1091 dialog.vbox.add(area)
1095 button = gtk.Button("QR")
1096 button.connect("clicked", showqrcode, treeview, liststore)
1098 hbox.pack_start(button,False)
1100 button = gtk.Button("Copy to clipboard")
1101 def copy2clipboard(w, treeview, liststore):
1103 path, col = treeview.get_cursor()
1105 address = liststore.get_value( liststore.get_iter(path), 0)
1106 if platform.system() == 'Windows':
1107 from Tkinter import Tk
1111 r.clipboard_append( address )
1114 c = gtk.clipboard_get()
1115 c.set_text( address )
1116 button.connect("clicked", copy2clipboard, treeview, liststore)
1118 hbox.pack_start(button,False)
1121 button = gtk.Button("Pay to")
1122 def payto(w, treeview, liststore):
1123 path, col = treeview.get_cursor()
1125 address = liststore.get_value( liststore.get_iter(path), 0)
1126 self.payto_entry.set_text( address )
1127 self.notebook.set_current_page(1)
1128 self.amount_entry.grab_focus()
1130 button.connect("clicked", payto, treeview, liststore)
1132 hbox.pack_start(button,False)
1135 vbox.pack_start(scroll,True)
1136 vbox.pack_start(hbox, False)
1139 def update_status_bar(self):
1140 interface = self.wallet.interface
1141 if self.funds_error:
1142 text = "Not enough funds"
1143 elif interface and interface.is_connected:
1144 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1145 if self.wallet.blocks == -1:
1146 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1147 text = "Connecting..."
1148 elif self.wallet.blocks == 0:
1149 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1150 text = "Server not ready"
1151 elif not self.wallet.up_to_date:
1152 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1153 text = "Synchronizing..."
1155 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1156 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1157 c, u = self.wallet.get_balance()
1158 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1159 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1161 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1162 self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(self.wallet.server, self.wallet.blocks))
1163 text = "Not connected"
1165 self.status_bar.pop(self.context_id)
1166 self.status_bar.push(self.context_id, text)
1168 if self.wallet.was_updated and self.wallet.up_to_date:
1169 self.update_history_tab()
1170 self.update_receiving_tab()
1171 # addressbook too...
1172 self.info.set_text( self.wallet.banner )
1173 self.wallet.was_updated = False
1176 def update_receiving_tab(self):
1177 self.recv_list.clear()
1178 for address in self.wallet.all_addresses():
1179 if self.wallet.is_change(address):continue
1180 label = self.wallet.labels.get(address)
1182 h = self.wallet.history.get(address,[])
1184 if not item['is_input'] : n=n+1
1185 tx = "None" if n==0 else "%d"%n
1186 self.recv_list.append((address, label, tx ))
1188 def update_sending_tab(self):
1189 # detect addresses that are not mine in history, add them here...
1190 self.addressbook_list.clear()
1191 for alias, v in self.wallet.aliases.items():
1193 label = self.wallet.labels.get(alias)
1194 self.addressbook_list.append((alias, label, '-'))
1196 for address in self.wallet.addressbook:
1197 label = self.wallet.labels.get(address)
1199 for item in self.wallet.tx_history.values():
1200 if address in item['outputs'] : n=n+1
1201 tx = "None" if n==0 else "%d"%n
1202 self.addressbook_list.append((address, label, tx))
1204 def update_history_tab(self):
1205 cursor = self.history_treeview.get_cursor()[0]
1206 self.history_list.clear()
1208 for tx in self.wallet.get_tx_history():
1209 tx_hash = tx['tx_hash']
1211 conf = self.wallet.blocks - tx['height'] + 1
1212 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
1213 conf_icon = gtk.STOCK_APPLY
1216 time_str = 'pending'
1217 conf_icon = gtk.STOCK_EXECUTE
1220 label = self.wallet.labels.get(tx_hash)
1221 is_default_label = (label == '') or (label is None)
1222 if is_default_label: label = tx['default_label']
1223 tooltip = tx_hash + "\n%d confirmations"%conf
1225 # tx = self.wallet.tx_history.get(tx_hash)
1226 details = "Transaction Details:\n\n" \
1227 + "Transaction ID:\n" + tx_hash + "\n\n" \
1228 + "Status: %d confirmations\n\n"%conf \
1229 + "Date: %s\n\n"%time_str \
1230 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
1231 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
1232 r = self.wallet.receipts.get(tx_hash)
1234 details += "\n_______________________________________" \
1235 + '\n\nSigned URI: ' + r[2] \
1236 + "\n\nSigned by: " + r[0] \
1237 + '\n\nSignature: ' + r[1]
1240 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1241 format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1242 if cursor: self.history_treeview.set_cursor( cursor )
1246 def newaddress_dialog(self, w):
1248 title = "New Contact"
1249 dialog = gtk.Dialog(title, parent=self.window,
1250 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1251 buttons= ("cancel", 0, "ok",1) )
1255 label_label = gtk.Label('Label:')
1256 label_label.set_size_request(120,10)
1258 label.pack_start(label_label)
1259 label_entry = gtk.Entry()
1261 label.pack_start(label_entry)
1263 dialog.vbox.pack_start(label, False, True, 5)
1265 address = gtk.HBox()
1266 address_label = gtk.Label('Address:')
1267 address_label.set_size_request(120,10)
1268 address_label.show()
1269 address.pack_start(address_label)
1270 address_entry = gtk.Entry()
1271 address_entry.show()
1272 address.pack_start(address_entry)
1274 dialog.vbox.pack_start(address, False, True, 5)
1276 result = dialog.run()
1277 address = address_entry.get_text()
1278 label = label_entry.get_text()
1282 if self.wallet.is_valid(address):
1283 self.wallet.addressbook.append(address)
1284 if label: self.wallet.labels[address] = label
1286 self.update_sending_tab()
1288 errorDialog = gtk.MessageDialog(
1290 flags=gtk.DIALOG_MODAL,
1291 buttons= gtk.BUTTONS_CLOSE,
1292 message_format = "Invalid address")
1295 errorDialog.destroy()
1299 class ElectrumGui():
1301 def __init__(self, wallet):
1302 self.wallet = wallet
1304 def main(self, url=None):
1305 ew = ElectrumWindow(self.wallet)
1306 if url: ew.set_url(url)
1309 def restore_or_create(self):
1310 return restore_create_dialog(self.wallet)