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
26 from decimal import Decimal
28 gtk.gdk.threads_init()
31 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
33 from wallet import format_satoshis
34 from interface import DEFAULT_SERVERS
36 def numbify(entry, is_int = False):
37 text = entry.get_text().strip()
39 if not is_int: chars +='.'
40 s = ''.join([i for i in text if i in chars])
45 s = s[:p] + '.' + s[p:p+8]
47 amount = int( Decimal(s) * 100000000 )
61 def show_seed_dialog(wallet, password, parent):
64 seed = wallet.pw_decode( wallet.seed, password)
66 show_message("Incorrect password")
68 dialog = gtk.MessageDialog(
70 flags = gtk.DIALOG_MODAL,
71 buttons = gtk.BUTTONS_OK,
72 message_format = "Your wallet generation seed is:\n\n" + seed \
73 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
74 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
75 dialog.set_title("Seed")
80 def restore_create_dialog(wallet):
82 # ask if the user wants to create a new wallet, or recover from a seed.
83 # if he wants to recover, and nothing is found, do not create wallet
84 dialog = gtk.Dialog("electrum", parent=None,
85 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
86 buttons= ("create", 0, "restore",1, "cancel",2) )
88 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
90 dialog.vbox.pack_start(label)
100 if not run_network_dialog( wallet, parent=None ): return False
104 wallet.new_seed(None)
106 wallet.init_mpk( wallet.seed )
107 wallet.up_to_date_event.clear()
110 # run a dialog indicating the seed, ask the user to remember it
111 show_seed_dialog(wallet, None, None)
114 change_password_dialog(wallet, None, None)
116 # ask for seed and gap.
117 run_recovery_dialog( wallet )
119 dialog = gtk.MessageDialog(
121 flags = gtk.DIALOG_MODAL,
122 buttons = gtk.BUTTONS_CANCEL,
123 message_format = "Please wait..." )
126 def recover_thread( wallet, dialog ):
127 wallet.init_mpk( wallet.seed ) # not encrypted at this point
128 wallet.up_to_date_event.clear()
131 if wallet.is_found():
132 # history and addressbook
133 wallet.update_tx_history()
134 wallet.fill_addressbook()
135 print "recovery successful"
137 gobject.idle_add( dialog.destroy )
139 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
142 if r==gtk.RESPONSE_CANCEL: return False
143 if not wallet.is_found:
144 show_message("No transactions found for this seed")
150 def run_recovery_dialog(wallet):
151 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
152 dialog = gtk.MessageDialog(
154 flags = gtk.DIALOG_MODAL,
155 buttons = gtk.BUTTONS_OK_CANCEL,
156 message_format = message)
159 dialog.set_default_response(gtk.RESPONSE_OK)
161 # ask seed, server and gap in the same dialog
162 seed_box = gtk.HBox()
163 seed_label = gtk.Label('Seed or mnemonic:')
164 seed_label.set_size_request(150,-1)
165 seed_box.pack_start(seed_label, False, False, 10)
167 seed_entry = gtk.Entry()
169 seed_entry.set_size_request(450,-1)
170 seed_box.pack_start(seed_entry, False, False, 10)
171 add_help_button(seed_box, '.')
173 vbox.pack_start(seed_box, False, False, 5)
176 gap_label = gtk.Label('Gap limit:')
177 gap_label.set_size_request(150,10)
179 gap.pack_start(gap_label,False, False, 10)
180 gap_entry = gtk.Entry()
181 gap_entry.set_text("%d"%wallet.gap_limit)
182 gap_entry.connect('changed', numbify, True)
184 gap.pack_start(gap_entry,False,False, 10)
185 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.')
187 vbox.pack_start(gap, False,False, 5)
191 gap = gap_entry.get_text()
192 seed = seed_entry.get_text()
195 if r==gtk.RESPONSE_CANCEL:
200 show_message("error")
207 print "not hex, trying decode"
208 seed = mnemonic.mn_decode( seed.split(' ') )
210 show_message("no seed")
214 wallet.gap_limit = gap
219 def run_settings_dialog(wallet, parent):
221 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
223 dialog = gtk.MessageDialog(
225 flags = gtk.DIALOG_MODAL,
226 buttons = gtk.BUTTONS_OK_CANCEL,
227 message_format = message)
230 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
232 dialog.set_image(image)
233 dialog.set_title("Settings")
236 dialog.set_default_response(gtk.RESPONSE_OK)
239 fee_entry = gtk.Entry()
240 fee_label = gtk.Label('Transaction fee:')
241 fee_label.set_size_request(150,10)
243 fee.pack_start(fee_label,False, False, 10)
244 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
245 fee_entry.connect('changed', numbify, False)
247 fee.pack_start(fee_entry,False,False, 10)
248 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
250 vbox.pack_start(fee, False,False, 5)
253 nz_entry = gtk.Entry()
254 nz_label = gtk.Label('Display zeros:')
255 nz_label.set_size_request(150,10)
257 nz.pack_start(nz_label,False, False, 10)
258 nz_entry.set_text( str( wallet.num_zeros ))
259 nz_entry.connect('changed', numbify, True)
261 nz.pack_start(nz_entry,False,False, 10)
262 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'")
264 vbox.pack_start(nz, False,False, 5)
268 fee = fee_entry.get_text()
269 nz = nz_entry.get_text()
272 if r==gtk.RESPONSE_CANCEL:
276 fee = int( 100000000 * Decimal(fee) )
278 show_message("error")
280 if wallet.fee != fee:
288 show_message("error")
290 if wallet.num_zeros != nz:
291 wallet.num_zeros = nz
297 def run_network_dialog( wallet, parent ):
299 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
300 interface = wallet.interface
302 if interface.is_connected:
303 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
305 status = "Not connected"
306 server = wallet.server
309 status = "Please choose a server."
310 server = random.choice( DEFAULT_SERVERS )
312 if not wallet.interface.servers:
314 for x in DEFAULT_SERVERS:
315 h,port,protocol = x.split(':')
316 servers_list.append( (h,[(protocol,port)] ) )
318 servers_list = wallet.interface.servers
321 for item in servers_list:
325 protocol, port = item2
329 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
330 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
331 dialog.set_title("Server")
332 dialog.set_image(image)
336 host_box = gtk.HBox()
337 host_label = gtk.Label('Connect to:')
338 host_label.set_size_request(100,-1)
340 host_box.pack_start(host_label, False, False, 10)
341 host_entry = gtk.Entry()
342 host_entry.set_size_request(200,-1)
343 host_entry.set_text(server)
345 host_box.pack_start(host_entry, False, False, 10)
346 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)')
350 p_box = gtk.HBox(False, 10)
353 p_label = gtk.Label('Protocol:')
354 p_label.set_size_request(100,-1)
356 p_box.pack_start(p_label, False, False, 10)
358 radio1 = gtk.RadioButton(None, "tcp")
359 p_box.pack_start(radio1, True, True, 0)
361 radio2 = gtk.RadioButton(radio1, "http")
362 p_box.pack_start(radio2, True, True, 0)
366 return unicode(host_entry.get_text()).split(':')
368 def set_button(protocol):
371 elif protocol == 'h':
374 def set_protocol(protocol):
375 host = current_line()[0]
377 if protocol not in pp.keys():
378 protocol = pp.keys()[0]
381 host_entry.set_text( host + ':' + port + ':' + protocol)
383 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
384 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
386 server_list = gtk.ListStore(str)
387 for host in plist.keys():
388 server_list.append([host])
390 treeview = gtk.TreeView(model=server_list)
393 if wallet.interface.servers:
394 label = 'Active Servers'
396 label = 'Default Servers'
398 tvcolumn = gtk.TreeViewColumn(label)
399 treeview.append_column(tvcolumn)
400 cell = gtk.CellRendererText()
401 tvcolumn.pack_start(cell, False)
402 tvcolumn.add_attribute(cell, 'text', 0)
404 scroll = gtk.ScrolledWindow()
405 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
409 vbox.pack_start(host_box, False,False, 5)
410 vbox.pack_start(p_box, True, True, 0)
411 vbox.pack_start(scroll)
413 def my_treeview_cb(treeview):
414 path, view_column = treeview.get_cursor()
415 host = server_list.get_value( server_list.get_iter(path), 0)
421 protocol = pp.keys()[0]
423 host_entry.set_text( host + ':' + port + ':' + protocol)
426 treeview.connect('cursor-changed', my_treeview_cb)
430 server = host_entry.get_text()
433 if r==gtk.RESPONSE_CANCEL:
437 wallet.set_server(server)
439 show_message("error:" + server)
448 def show_message(message, parent=None):
449 dialog = gtk.MessageDialog(
451 flags = gtk.DIALOG_MODAL,
452 buttons = gtk.BUTTONS_CLOSE,
453 message_format = message )
458 def password_line(label):
459 password = gtk.HBox()
460 password_label = gtk.Label(label)
461 password_label.set_size_request(120,10)
462 password_label.show()
463 password.pack_start(password_label,False, False, 10)
464 password_entry = gtk.Entry()
465 password_entry.set_size_request(300,-1)
466 password_entry.set_visibility(False)
467 password_entry.show()
468 password.pack_start(password_entry,False,False, 10)
470 return password, password_entry
472 def password_dialog(parent):
473 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
474 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
475 dialog.get_image().set_visible(False)
476 current_pw, current_pw_entry = password_line('Password:')
477 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
478 dialog.vbox.pack_start(current_pw, False, True, 0)
480 result = dialog.run()
481 pw = current_pw_entry.get_text()
483 if result != gtk.RESPONSE_CANCEL: return pw
485 def change_password_dialog(wallet, parent, icon):
487 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'
489 msg = "Please choose a password to encrypt your wallet keys"
491 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
492 dialog.set_title("Change password")
494 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
496 dialog.set_image(image)
498 if wallet.use_encryption:
499 current_pw, current_pw_entry = password_line('Current password:')
500 dialog.vbox.pack_start(current_pw, False, True, 0)
502 password, password_entry = password_line('New password:')
503 dialog.vbox.pack_start(password, False, True, 5)
504 password2, password2_entry = password_line('Confirm password:')
505 dialog.vbox.pack_start(password2, False, True, 5)
508 result = dialog.run()
509 password = current_pw_entry.get_text() if wallet.use_encryption else None
510 new_password = password_entry.get_text()
511 new_password2 = password2_entry.get_text()
513 if result == gtk.RESPONSE_CANCEL:
517 seed = wallet.pw_decode( wallet.seed, password)
519 show_message("Incorrect password")
522 if new_password != new_password2:
523 show_message("passwords do not match")
526 wallet.update_password(seed, new_password)
529 if wallet.use_encryption:
530 icon.set_tooltip_text('wallet is encrypted')
532 icon.set_tooltip_text('wallet is unencrypted')
535 def add_help_button(hbox, message):
536 button = gtk.Button('?')
537 button.connect("clicked", lambda x: show_message(message))
539 hbox.pack_start(button,False, False)
542 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
544 gobject.type_register(MyWindow)
545 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
546 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
549 class ElectrumWindow:
551 def show_message(self, msg):
552 show_message(msg, self.window)
554 def __init__(self, wallet):
556 self.funds_error = False # True if not enough funds
558 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
559 self.window.set_title(APP_NAME + " " + self.wallet.electrum_version)
560 self.window.connect("destroy", gtk.main_quit)
561 self.window.set_border_width(0)
562 self.window.connect('mykeypress', gtk.main_quit)
563 self.window.set_default_size(720, 350)
567 self.notebook = gtk.Notebook()
568 self.create_history_tab()
569 self.create_send_tab()
570 self.create_recv_tab()
571 self.create_book_tab()
572 self.create_about_tab()
574 vbox.pack_start(self.notebook, True, True, 2)
576 self.status_bar = gtk.Statusbar()
577 vbox.pack_start(self.status_bar, False, False, 0)
579 self.status_image = gtk.Image()
580 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
581 self.status_image.set_alignment(True, 0.5 )
582 self.status_image.show()
584 self.network_button = gtk.Button()
585 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
586 self.network_button.add(self.status_image)
587 self.network_button.set_relief(gtk.RELIEF_NONE)
588 self.network_button.show()
589 self.status_bar.pack_end(self.network_button, False, False)
591 def seedb(w, wallet):
592 if wallet.use_encryption:
593 password = password_dialog(self.window)
594 if not password: return
595 else: password = None
596 show_seed_dialog(wallet, password, self.window)
597 button = gtk.Button('S')
598 button.connect("clicked", seedb, wallet )
599 button.set_relief(gtk.RELIEF_NONE)
601 self.status_bar.pack_end(button,False, False)
603 settings_icon = gtk.Image()
604 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
605 settings_icon.set_alignment(0.5, 0.5)
606 settings_icon.set_size_request(16,16 )
609 prefs_button = gtk.Button()
610 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
611 prefs_button.add(settings_icon)
612 prefs_button.set_tooltip_text("Settings")
613 prefs_button.set_relief(gtk.RELIEF_NONE)
615 self.status_bar.pack_end(prefs_button,False,False)
617 pw_icon = gtk.Image()
618 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
619 pw_icon.set_alignment(0.5, 0.5)
620 pw_icon.set_size_request(16,16 )
623 password_button = gtk.Button()
624 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
625 password_button.add(pw_icon)
626 password_button.set_relief(gtk.RELIEF_NONE)
627 password_button.show()
628 self.status_bar.pack_end(password_button,False,False)
630 self.window.add(vbox)
631 self.window.show_all()
634 self.context_id = self.status_bar.get_context_id("statusbar")
635 self.update_status_bar()
637 def update_status_bar_thread():
639 gobject.idle_add( self.update_status_bar )
643 def check_recipient_thread():
647 if self.payto_entry.is_focus():
649 r = self.payto_entry.get_text()
653 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
655 to_address = self.wallet.get_alias(r, interactive=False)
659 s = r + ' <' + to_address + '>'
660 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
663 thread.start_new_thread(update_status_bar_thread, ())
664 thread.start_new_thread(check_recipient_thread, ())
665 self.notebook.set_current_page(0)
668 def add_tab(self, page, name):
669 tab_label = gtk.Label(name)
671 self.notebook.append_page(page, tab_label)
674 def create_send_tab(self):
676 page = vbox = gtk.VBox()
680 payto_label = gtk.Label('Pay to:')
681 payto_label.set_size_request(100,-1)
682 payto.pack_start(payto_label, False)
683 payto_entry = gtk.Entry()
684 payto_entry.set_size_request(450, 26)
685 payto.pack_start(payto_entry, False)
686 vbox.pack_start(payto, False, False, 5)
689 message_label = gtk.Label('Description:')
690 message_label.set_size_request(100,-1)
691 message.pack_start(message_label, False)
692 message_entry = gtk.Entry()
693 message_entry.set_size_request(450, 26)
694 message.pack_start(message_entry, False)
695 vbox.pack_start(message, False, False, 5)
697 amount_box = gtk.HBox()
698 amount_label = gtk.Label('Amount:')
699 amount_label.set_size_request(100,-1)
700 amount_box.pack_start(amount_label, False)
701 amount_entry = gtk.Entry()
702 amount_entry.set_size_request(120, -1)
703 amount_box.pack_start(amount_entry, False)
704 vbox.pack_start(amount_box, False, False, 5)
706 self.fee_box = fee_box = gtk.HBox()
707 fee_label = gtk.Label('Fee:')
708 fee_label.set_size_request(100,-1)
709 fee_box.pack_start(fee_label, False)
710 fee_entry = gtk.Entry()
711 fee_entry.set_size_request(60, 26)
712 fee_box.pack_start(fee_entry, False)
713 vbox.pack_start(fee_box, False, False, 5)
716 empty_label = gtk.Label('')
717 empty_label.set_size_request(100,-1)
718 end_box.pack_start(empty_label, False)
719 send_button = gtk.Button("Send")
721 end_box.pack_start(send_button, False, False, 0)
722 clear_button = gtk.Button("Clear")
724 end_box.pack_start(clear_button, False, False, 15)
725 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
726 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
728 vbox.pack_start(end_box, False, False, 5)
730 # display this line only if there is a signature
731 payto_sig = gtk.HBox()
732 payto_sig_id = gtk.Label('')
733 payto_sig.pack_start(payto_sig_id, False)
734 vbox.pack_start(payto_sig, True, True, 5)
737 self.user_fee = False
739 def entry_changed( entry, is_fee ):
740 self.funds_error = False
741 amount = numbify(amount_entry)
742 fee = numbify(fee_entry)
743 if not is_fee: fee = None
746 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
748 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
751 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
752 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
753 send_button.set_sensitive(True)
755 send_button.set_sensitive(False)
756 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
757 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
758 self.funds_error = True
760 amount_entry.connect('changed', entry_changed, False)
761 fee_entry.connect('changed', entry_changed, True)
763 self.payto_entry = payto_entry
764 self.payto_fee_entry = fee_entry
765 self.payto_sig_id = payto_sig_id
766 self.payto_sig = payto_sig
767 self.amount_entry = amount_entry
768 self.message_entry = message_entry
769 self.add_tab(page, 'Send')
771 def set_frozen(self,entry,frozen):
773 entry.set_editable(False)
774 entry.set_has_frame(False)
775 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
777 entry.set_editable(True)
778 entry.set_has_frame(True)
779 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
781 def set_url(self, url):
782 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
783 self.notebook.set_current_page(1)
784 self.payto_entry.set_text(payto)
785 self.message_entry.set_text(message)
786 self.amount_entry.set_text(amount)
788 self.set_frozen(self.payto_entry,True)
789 self.set_frozen(self.amount_entry,True)
790 self.set_frozen(self.message_entry,True)
791 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
793 self.payto_sig.set_visible(False)
795 def create_about_tab(self):
800 tv.set_editable(False)
801 tv.set_cursor_visible(False)
802 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
804 self.info = tv.get_buffer()
805 self.add_tab(page, 'Wall')
807 def do_clear(self, w, data):
808 self.payto_sig.set_visible(False)
809 self.payto_fee_entry.set_text('')
810 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
811 self.set_frozen(entry,False)
814 def question(self,msg):
815 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
817 result = dialog.run()
819 return result == gtk.RESPONSE_OK
821 def do_send(self, w, data):
822 payto_entry, label_entry, amount_entry, fee_entry = data
823 label = label_entry.get_text()
824 r = payto_entry.get_text()
827 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
828 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
831 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
835 self.update_sending_tab()
838 to_address = m2.group(5)
842 if not self.wallet.is_valid(to_address):
843 self.show_message( "invalid bitcoin address:\n"+to_address)
847 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
849 self.show_message( "invalid amount")
852 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
854 self.show_message( "invalid fee")
857 if self.wallet.use_encryption:
858 password = password_dialog(self.window)
865 tx = self.wallet.mktx( to_address, amount, label, password, fee )
866 except BaseException, e:
867 self.show_message(e.message)
870 status, msg = self.wallet.sendtx( tx )
872 self.show_message( "payment sent.\n" + msg )
873 payto_entry.set_text("")
874 label_entry.set_text("")
875 amount_entry.set_text("")
876 fee_entry.set_text("")
878 self.update_sending_tab()
880 self.show_message( msg )
883 def treeview_button_press(self, treeview, event):
884 if event.type == gtk.gdk._2BUTTON_PRESS:
885 c = treeview.get_cursor()[0]
886 if treeview == self.history_treeview:
887 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
888 self.show_message(tx_details)
889 elif treeview == self.contacts_treeview:
890 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
891 a = self.wallet.aliases.get(m)
893 if a[0] in self.wallet.authorities.keys():
894 s = self.wallet.authorities.get(a[0])
897 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
898 self.show_message(msg)
901 def treeview_key_press(self, treeview, event):
902 c = treeview.get_cursor()[0]
903 if event.keyval == gtk.keysyms.Up:
905 treeview.parent.grab_focus()
906 treeview.set_cursor((0,))
907 elif event.keyval == gtk.keysyms.Return:
908 if treeview == self.history_treeview:
909 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
910 self.show_message(tx_details)
911 elif treeview == self.contacts_treeview:
912 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
913 a = self.wallet.aliases.get(m)
915 if a[0] in self.wallet.authorities.keys():
916 s = self.wallet.authorities.get(a[0])
919 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
920 self.show_message(msg)
924 def create_history_tab(self):
926 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
927 treeview = gtk.TreeView(model=self.history_list)
928 self.history_treeview = treeview
929 treeview.set_tooltip_column(7)
931 treeview.connect('key-press-event', self.treeview_key_press)
932 treeview.connect('button-press-event', self.treeview_button_press)
934 tvcolumn = gtk.TreeViewColumn('')
935 treeview.append_column(tvcolumn)
936 cell = gtk.CellRendererPixbuf()
937 tvcolumn.pack_start(cell, False)
938 tvcolumn.set_attributes(cell, stock_id=1)
940 tvcolumn = gtk.TreeViewColumn('Date')
941 treeview.append_column(tvcolumn)
942 cell = gtk.CellRendererText()
943 tvcolumn.pack_start(cell, False)
944 tvcolumn.add_attribute(cell, 'text', 2)
946 tvcolumn = gtk.TreeViewColumn('Description')
947 treeview.append_column(tvcolumn)
948 cell = gtk.CellRendererText()
949 cell.set_property('foreground', 'grey')
950 cell.set_property('family', MONOSPACE_FONT)
951 cell.set_property('editable', True)
952 def edited_cb(cell, path, new_text, h_list):
953 tx = h_list.get_value( h_list.get_iter(path), 0)
954 self.wallet.labels[tx] = new_text
956 self.update_history_tab()
957 cell.connect('edited', edited_cb, self.history_list)
958 def editing_started(cell, entry, path, h_list):
959 tx = h_list.get_value( h_list.get_iter(path), 0)
960 if not self.wallet.labels.get(tx): entry.set_text('')
961 cell.connect('editing-started', editing_started, self.history_list)
962 tvcolumn.set_expand(True)
963 tvcolumn.pack_start(cell, True)
964 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
966 tvcolumn = gtk.TreeViewColumn('Amount')
967 treeview.append_column(tvcolumn)
968 cell = gtk.CellRendererText()
969 cell.set_alignment(1, 0.5)
970 cell.set_property('family', MONOSPACE_FONT)
971 tvcolumn.pack_start(cell, False)
972 tvcolumn.add_attribute(cell, 'text', 5)
974 tvcolumn = gtk.TreeViewColumn('Balance')
975 treeview.append_column(tvcolumn)
976 cell = gtk.CellRendererText()
977 cell.set_alignment(1, 0.5)
978 cell.set_property('family', MONOSPACE_FONT)
979 tvcolumn.pack_start(cell, False)
980 tvcolumn.add_attribute(cell, 'text', 6)
982 tvcolumn = gtk.TreeViewColumn('Tooltip')
983 treeview.append_column(tvcolumn)
984 cell = gtk.CellRendererText()
985 tvcolumn.pack_start(cell, False)
986 tvcolumn.add_attribute(cell, 'text', 7)
987 tvcolumn.set_visible(False)
989 scroll = gtk.ScrolledWindow()
990 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
993 self.add_tab(scroll, 'History')
994 self.update_history_tab()
997 def create_recv_tab(self):
998 self.recv_list = gtk.ListStore(str, str, str)
999 self.add_tab( self.make_address_list(True), 'Receive')
1000 self.update_receiving_tab()
1002 def create_book_tab(self):
1003 self.addressbook_list = gtk.ListStore(str, str, str)
1004 self.add_tab( self.make_address_list(False), 'Contacts')
1005 self.update_sending_tab()
1007 def make_address_list(self, is_recv):
1008 liststore = self.recv_list if is_recv else self.addressbook_list
1009 treeview = gtk.TreeView(model= liststore)
1010 treeview.connect('key-press-event', self.treeview_key_press)
1011 treeview.connect('button-press-event', self.treeview_button_press)
1014 self.contacts_treeview = treeview
1016 tvcolumn = gtk.TreeViewColumn('Address')
1017 treeview.append_column(tvcolumn)
1018 cell = gtk.CellRendererText()
1019 cell.set_property('family', MONOSPACE_FONT)
1020 tvcolumn.pack_start(cell, True)
1021 tvcolumn.add_attribute(cell, 'text', 0)
1023 tvcolumn = gtk.TreeViewColumn('Label')
1024 tvcolumn.set_expand(True)
1025 treeview.append_column(tvcolumn)
1026 cell = gtk.CellRendererText()
1027 cell.set_property('editable', True)
1028 def edited_cb2(cell, path, new_text, liststore):
1029 address = liststore.get_value( liststore.get_iter(path), 0)
1030 self.wallet.labels[address] = new_text
1032 self.wallet.update_tx_labels()
1033 self.update_receiving_tab()
1034 self.update_sending_tab()
1035 self.update_history_tab()
1036 cell.connect('edited', edited_cb2, liststore)
1037 tvcolumn.pack_start(cell, True)
1038 tvcolumn.add_attribute(cell, 'text', 1)
1040 tvcolumn = gtk.TreeViewColumn('Tx')
1041 treeview.append_column(tvcolumn)
1042 cell = gtk.CellRendererText()
1043 tvcolumn.pack_start(cell, True)
1044 tvcolumn.add_attribute(cell, 'text', 2)
1046 scroll = gtk.ScrolledWindow()
1047 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1048 scroll.add(treeview)
1052 button = gtk.Button("New")
1053 button.connect("clicked", self.newaddress_dialog)
1055 hbox.pack_start(button,False)
1057 def showqrcode(w, treeview, liststore):
1058 path, col = treeview.get_cursor()
1060 address = liststore.get_value(liststore.get_iter(path), 0)
1061 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1065 size = qr.getModuleCount()*boxsize
1066 def area_expose_cb(area, event):
1067 style = area.get_style()
1068 k = qr.getModuleCount()
1071 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1072 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1073 area = gtk.DrawingArea()
1074 area.set_size_request(size, size)
1075 area.connect("expose-event", area_expose_cb)
1077 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1078 dialog.vbox.add(area)
1082 button = gtk.Button("QR")
1083 button.connect("clicked", showqrcode, treeview, liststore)
1085 hbox.pack_start(button,False)
1087 button = gtk.Button("Copy to clipboard")
1088 def copy2clipboard(w, treeview, liststore):
1090 path, col = treeview.get_cursor()
1092 address = liststore.get_value( liststore.get_iter(path), 0)
1093 if platform.system() == 'Windows':
1094 from Tkinter import Tk
1098 r.clipboard_append( address )
1101 c = gtk.clipboard_get()
1102 c.set_text( address )
1103 button.connect("clicked", copy2clipboard, treeview, liststore)
1105 hbox.pack_start(button,False)
1108 button = gtk.Button("Pay to")
1109 def payto(w, treeview, liststore):
1110 path, col = treeview.get_cursor()
1112 address = liststore.get_value( liststore.get_iter(path), 0)
1113 self.payto_entry.set_text( address )
1114 self.notebook.set_current_page(1)
1115 self.amount_entry.grab_focus()
1117 button.connect("clicked", payto, treeview, liststore)
1119 hbox.pack_start(button,False)
1122 vbox.pack_start(scroll,True)
1123 vbox.pack_start(hbox, False)
1126 def update_status_bar(self):
1127 interface = self.wallet.interface
1128 if self.funds_error:
1129 text = "Not enough funds"
1130 elif interface.is_connected:
1131 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1132 if self.wallet.blocks == -1:
1133 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1134 text = "Connecting..."
1135 elif self.wallet.blocks == 0:
1136 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1137 text = "Server not ready"
1138 elif not self.wallet.up_to_date:
1139 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1140 text = "Synchronizing..."
1142 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1143 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks))
1144 c, u = self.wallet.get_balance()
1145 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1146 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1148 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1149 self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(interface.host, self.wallet.blocks))
1150 text = "Not connected"
1152 self.status_bar.pop(self.context_id)
1153 self.status_bar.push(self.context_id, text)
1155 if self.wallet.was_updated and self.wallet.up_to_date:
1156 self.update_history_tab()
1157 self.update_receiving_tab()
1158 # addressbook too...
1159 self.info.set_text( self.wallet.banner )
1160 self.wallet.was_updated = False
1163 def update_receiving_tab(self):
1164 self.recv_list.clear()
1165 for address in self.wallet.all_addresses():
1166 if self.wallet.is_change(address):continue
1167 label = self.wallet.labels.get(address)
1169 h = self.wallet.history.get(address,[])
1171 if not item['is_input'] : n=n+1
1172 tx = "None" if n==0 else "%d"%n
1173 self.recv_list.append((address, label, tx ))
1175 def update_sending_tab(self):
1176 # detect addresses that are not mine in history, add them here...
1177 self.addressbook_list.clear()
1178 for alias, v in self.wallet.aliases.items():
1180 label = self.wallet.labels.get(alias)
1181 self.addressbook_list.append((alias, label, '-'))
1183 for address in self.wallet.addressbook:
1184 label = self.wallet.labels.get(address)
1186 for item in self.wallet.tx_history.values():
1187 if address in item['outputs'] : n=n+1
1188 tx = "None" if n==0 else "%d"%n
1189 self.addressbook_list.append((address, label, tx))
1191 def update_history_tab(self):
1192 cursor = self.history_treeview.get_cursor()[0]
1193 self.history_list.clear()
1195 for tx in self.wallet.get_tx_history():
1196 tx_hash = tx['tx_hash']
1198 conf = self.wallet.blocks - tx['height'] + 1
1199 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
1200 conf_icon = gtk.STOCK_APPLY
1203 time_str = 'pending'
1204 conf_icon = gtk.STOCK_EXECUTE
1207 label = self.wallet.labels.get(tx_hash)
1208 is_default_label = (label == '') or (label is None)
1209 if is_default_label: label = tx['default_label']
1210 tooltip = tx_hash + "\n%d confirmations"%conf
1212 # tx = self.wallet.tx_history.get(tx_hash)
1213 details = "Transaction Details:\n\n" \
1214 + "Transaction ID:\n" + tx_hash + "\n\n" \
1215 + "Status: %d confirmations\n\n"%conf \
1216 + "Date: %s\n\n"%time_str \
1217 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
1218 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
1219 r = self.wallet.receipts.get(tx_hash)
1221 details += "\n_______________________________________" \
1222 + '\n\nSigned URI: ' + r[2] \
1223 + "\n\nSigned by: " + r[0] \
1224 + '\n\nSignature: ' + r[1]
1227 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1228 format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1229 if cursor: self.history_treeview.set_cursor( cursor )
1233 def newaddress_dialog(self, w):
1235 title = "New Contact"
1236 dialog = gtk.Dialog(title, parent=self.window,
1237 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1238 buttons= ("cancel", 0, "ok",1) )
1242 label_label = gtk.Label('Label:')
1243 label_label.set_size_request(120,10)
1245 label.pack_start(label_label)
1246 label_entry = gtk.Entry()
1248 label.pack_start(label_entry)
1250 dialog.vbox.pack_start(label, False, True, 5)
1252 address = gtk.HBox()
1253 address_label = gtk.Label('Address:')
1254 address_label.set_size_request(120,10)
1255 address_label.show()
1256 address.pack_start(address_label)
1257 address_entry = gtk.Entry()
1258 address_entry.show()
1259 address.pack_start(address_entry)
1261 dialog.vbox.pack_start(address, False, True, 5)
1263 result = dialog.run()
1264 address = address_entry.get_text()
1265 label = label_entry.get_text()
1269 if self.wallet.is_valid(address):
1270 self.wallet.addressbook.append(address)
1271 if label: self.wallet.labels[address] = label
1273 self.update_sending_tab()
1275 errorDialog = gtk.MessageDialog(
1277 flags=gtk.DIALOG_MODAL,
1278 buttons= gtk.BUTTONS_CLOSE,
1279 message_format = "Invalid address")
1282 errorDialog.destroy()
1286 class ElectrumGui():
1288 def __init__(self, wallet):
1289 self.wallet = wallet
1291 def main(self, url=None):
1292 ew = ElectrumWindow(self.wallet)
1293 if url: ew.set_url(url)
1296 def restore_or_create(self):
1297 return restore_create_dialog(self.wallet)