3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import thread, time, ast, sys, re
21 import socket, traceback
25 from decimal import Decimal
26 from electrum.util import print_error
27 from electrum.bitcoin import is_valid
28 from electrum import mnemonic, pyqrnative, WalletStorage, Wallet
30 gtk.gdk.threads_init()
33 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
35 from electrum.util import format_satoshis
36 from electrum.network import DEFAULT_SERVERS
37 from electrum.bitcoin import MIN_RELAY_TX_FEE
39 def numbify(entry, is_int = False):
40 text = entry.get_text().strip()
42 if not is_int: chars +='.'
43 s = ''.join([i for i in text if i in chars])
48 s = s[:p] + '.' + s[p:p+8]
50 amount = int( Decimal(s) * 100000000 )
64 def show_seed_dialog(wallet, password, parent):
66 show_message("No seed")
69 seed = wallet.decode_seed(password)
71 show_message("Incorrect password")
73 dialog = gtk.MessageDialog(
75 flags = gtk.DIALOG_MODAL,
76 buttons = gtk.BUTTONS_OK,
77 message_format = "Your wallet generation seed is:\n\n" + seed \
78 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
79 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
80 dialog.set_title("Seed")
85 def restore_create_dialog(wallet):
87 # ask if the user wants to create a new wallet, or recover from a seed.
88 # if he wants to recover, and nothing is found, do not create wallet
89 dialog = gtk.Dialog("electrum", parent=None,
90 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
91 buttons= ("create", 0, "restore",1, "cancel",2) )
93 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
95 dialog.vbox.pack_start(label)
100 if r==2: return False
101 return 'restore' if r==1 else 'create'
105 def run_recovery_dialog(wallet):
106 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
107 dialog = gtk.MessageDialog(
109 flags = gtk.DIALOG_MODAL,
110 buttons = gtk.BUTTONS_OK_CANCEL,
111 message_format = message)
114 dialog.set_default_response(gtk.RESPONSE_OK)
116 # ask seed, server and gap in the same dialog
117 seed_box = gtk.HBox()
118 seed_label = gtk.Label('Seed or mnemonic:')
119 seed_label.set_size_request(150,-1)
120 seed_box.pack_start(seed_label, False, False, 10)
122 seed_entry = gtk.Entry()
124 seed_entry.set_size_request(450,-1)
125 seed_box.pack_start(seed_entry, False, False, 10)
126 add_help_button(seed_box, '.')
128 vbox.pack_start(seed_box, False, False, 5)
131 gap_label = gtk.Label('Gap limit:')
132 gap_label.set_size_request(150,10)
134 gap.pack_start(gap_label,False, False, 10)
135 gap_entry = gtk.Entry()
136 gap_entry.set_text("%d"%wallet.gap_limit)
137 gap_entry.connect('changed', numbify, True)
139 gap.pack_start(gap_entry,False,False, 10)
140 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.')
142 vbox.pack_start(gap, False,False, 5)
146 gap = gap_entry.get_text()
147 seed = seed_entry.get_text()
150 if r==gtk.RESPONSE_CANCEL:
156 show_message("error")
162 print_error("Warning: Not hex, trying decode")
163 seed = mnemonic.mn_decode( seed.split(' ') )
165 show_message("no seed")
172 def run_settings_dialog(wallet, parent):
174 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
176 dialog = gtk.MessageDialog(
178 flags = gtk.DIALOG_MODAL,
179 buttons = gtk.BUTTONS_OK_CANCEL,
180 message_format = message)
183 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
185 dialog.set_image(image)
186 dialog.set_title("Settings")
189 dialog.set_default_response(gtk.RESPONSE_OK)
192 fee_entry = gtk.Entry()
193 fee_label = gtk.Label('Transaction fee:')
194 fee_label.set_size_request(150,10)
196 fee.pack_start(fee_label,False, False, 10)
197 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
198 fee_entry.connect('changed', numbify, False)
200 fee.pack_start(fee_entry,False,False, 10)
201 add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001')
203 vbox.pack_start(fee, False,False, 5)
206 nz_entry = gtk.Entry()
207 nz_label = gtk.Label('Display zeros:')
208 nz_label.set_size_request(150,10)
210 nz.pack_start(nz_label,False, False, 10)
211 nz_entry.set_text( str( wallet.num_zeros ))
212 nz_entry.connect('changed', numbify, True)
214 nz.pack_start(nz_entry,False,False, 10)
215 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'")
217 vbox.pack_start(nz, False,False, 5)
221 gui_label = gtk.Label('Default GUI:')
222 gui_label.set_size_request(150,10)
224 gui_box.pack_start(gui_label,False, False, 10)
225 gui_combo = gtk.combo_box_new_text()
226 gui_names = ['lite', 'classic', 'gtk', 'text']
227 for name in gui_names: gui_combo.append_text(name.capitalize())
229 gui_box.pack_start(gui_combo,False, False, 10)
230 gui_combo.set_active( gui_names.index( wallet.config.get("gui","lite")) )
232 add_help_button(gui_box, "Select which GUI mode to use at start up.")
234 vbox.pack_start(gui_box, False,False, 5)
238 fee = fee_entry.get_text()
239 nz = nz_entry.get_text()
240 gui = gui_names[ gui_combo.get_active()]
243 if r==gtk.RESPONSE_CANCEL:
247 fee = int( 100000000 * Decimal(fee) )
249 show_message("error")
257 show_message("error")
259 if wallet.num_zeros != nz:
260 wallet.num_zeros = nz
263 wallet.config.set_key('gui',gui,True)
268 def run_network_dialog( wallet, parent ):
270 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
271 interface = wallet.network.interface
273 if interface.is_connected:
274 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.network.blockchain.height)
276 status = "Not connected"
279 status = "Please choose a server.\nSelect cancel if you are offline."
281 server = interface.server
282 servers = wallet.network.get_servers()
284 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
285 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
286 dialog.set_title("Server")
287 dialog.set_image(image)
291 host_box = gtk.HBox()
292 host_label = gtk.Label('Connect to:')
293 host_label.set_size_request(100,-1)
295 host_box.pack_start(host_label, False, False, 10)
296 host_entry = gtk.Entry()
297 host_entry.set_size_request(200,-1)
298 host_entry.set_text(server)
300 host_box.pack_start(host_entry, False, False, 10)
301 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)')
305 p_box = gtk.HBox(False, 10)
308 p_label = gtk.Label('Protocol:')
309 p_label.set_size_request(100,-1)
311 p_box.pack_start(p_label, False, False, 10)
313 radio1 = gtk.RadioButton(None, "tcp")
314 p_box.pack_start(radio1, True, True, 0)
316 radio2 = gtk.RadioButton(radio1, "http")
317 p_box.pack_start(radio2, True, True, 0)
321 return unicode(host_entry.get_text()).split(':')
323 def set_button(protocol):
326 elif protocol == 'h':
329 def set_protocol(protocol):
330 host = current_line()[0]
332 if protocol not in pp.keys():
333 protocol = pp.keys()[0]
336 host_entry.set_text( host + ':' + port + ':' + protocol)
338 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
339 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
341 server_list = gtk.ListStore(str)
342 for host in servers.keys():
343 server_list.append([host])
345 treeview = gtk.TreeView(model=server_list)
348 if interface.servers:
349 label = 'Active Servers'
351 label = 'Default Servers'
353 tvcolumn = gtk.TreeViewColumn(label)
354 treeview.append_column(tvcolumn)
355 cell = gtk.CellRendererText()
356 tvcolumn.pack_start(cell, False)
357 tvcolumn.add_attribute(cell, 'text', 0)
359 scroll = gtk.ScrolledWindow()
360 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
364 vbox.pack_start(host_box, False,False, 5)
365 vbox.pack_start(p_box, True, True, 0)
366 vbox.pack_start(scroll)
368 def my_treeview_cb(treeview):
369 path, view_column = treeview.get_cursor()
370 host = server_list.get_value( server_list.get_iter(path), 0)
376 protocol = pp.keys()[0]
378 host_entry.set_text( host + ':' + port + ':' + protocol)
381 treeview.connect('cursor-changed', my_treeview_cb)
385 server = host_entry.get_text()
388 if r==gtk.RESPONSE_CANCEL:
392 interface.set_server(server)
394 show_message("error:" + server)
398 wallet.config.set_key("server", server, True)
403 def show_message(message, parent=None):
404 dialog = gtk.MessageDialog(
406 flags = gtk.DIALOG_MODAL,
407 buttons = gtk.BUTTONS_CLOSE,
408 message_format = message )
413 def password_line(label):
414 password = gtk.HBox()
415 password_label = gtk.Label(label)
416 password_label.set_size_request(120,10)
417 password_label.show()
418 password.pack_start(password_label,False, False, 10)
419 password_entry = gtk.Entry()
420 password_entry.set_size_request(300,-1)
421 password_entry.set_visibility(False)
422 password_entry.show()
423 password.pack_start(password_entry,False,False, 10)
425 return password, password_entry
427 def password_dialog(parent):
428 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
429 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
430 dialog.get_image().set_visible(False)
431 current_pw, current_pw_entry = password_line('Password:')
432 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
433 dialog.vbox.pack_start(current_pw, False, True, 0)
435 result = dialog.run()
436 pw = current_pw_entry.get_text()
438 if result != gtk.RESPONSE_CANCEL: return pw
440 def change_password_dialog(wallet, parent, icon):
442 show_message("No seed")
446 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'
448 msg = "Please choose a password to encrypt your wallet keys"
450 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
451 dialog.set_title("Change password")
453 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
455 dialog.set_image(image)
457 if wallet.use_encryption:
458 current_pw, current_pw_entry = password_line('Current password:')
459 dialog.vbox.pack_start(current_pw, False, True, 0)
461 password, password_entry = password_line('New password:')
462 dialog.vbox.pack_start(password, False, True, 5)
463 password2, password2_entry = password_line('Confirm password:')
464 dialog.vbox.pack_start(password2, False, True, 5)
467 result = dialog.run()
468 password = current_pw_entry.get_text() if wallet.use_encryption else None
469 new_password = password_entry.get_text()
470 new_password2 = password2_entry.get_text()
472 if result == gtk.RESPONSE_CANCEL:
476 seed = wallet.decode_seed(password)
478 show_message("Incorrect password")
481 if new_password != new_password2:
482 show_message("passwords do not match")
485 wallet.update_password(seed, password, new_password)
488 if wallet.use_encryption:
489 icon.set_tooltip_text('wallet is encrypted')
491 icon.set_tooltip_text('wallet is unencrypted')
494 def add_help_button(hbox, message):
495 button = gtk.Button('?')
496 button.connect("clicked", lambda x: show_message(message))
498 hbox.pack_start(button,False, False)
501 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
503 gobject.type_register(MyWindow)
504 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
505 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
508 class ElectrumWindow:
510 def show_message(self, msg):
511 show_message(msg, self.window)
513 def __init__(self, wallet, config):
516 self.funds_error = False # True if not enough funds
517 self.num_zeros = int(self.config.get('num_zeros',0))
519 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
520 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
521 if not self.wallet.seed: title += ' [seedless]'
522 self.window.set_title(title)
523 self.window.connect("destroy", gtk.main_quit)
524 self.window.set_border_width(0)
525 self.window.connect('mykeypress', gtk.main_quit)
526 self.window.set_default_size(720, 350)
527 self.wallet_updated = False
531 self.notebook = gtk.Notebook()
532 self.create_history_tab()
534 self.create_send_tab()
535 self.create_recv_tab()
536 self.create_book_tab()
537 self.create_about_tab()
539 vbox.pack_start(self.notebook, True, True, 2)
541 self.status_bar = gtk.Statusbar()
542 vbox.pack_start(self.status_bar, False, False, 0)
544 self.status_image = gtk.Image()
545 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
546 self.status_image.set_alignment(True, 0.5 )
547 self.status_image.show()
549 self.network_button = gtk.Button()
550 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
551 self.network_button.add(self.status_image)
552 self.network_button.set_relief(gtk.RELIEF_NONE)
553 self.network_button.show()
554 self.status_bar.pack_end(self.network_button, False, False)
557 def seedb(w, wallet):
558 if wallet.use_encryption:
559 password = password_dialog(self.window)
560 if not password: return
561 else: password = None
562 show_seed_dialog(wallet, password, self.window)
563 button = gtk.Button('S')
564 button.connect("clicked", seedb, wallet )
565 button.set_relief(gtk.RELIEF_NONE)
567 self.status_bar.pack_end(button,False, False)
569 settings_icon = gtk.Image()
570 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
571 settings_icon.set_alignment(0.5, 0.5)
572 settings_icon.set_size_request(16,16 )
575 prefs_button = gtk.Button()
576 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
577 prefs_button.add(settings_icon)
578 prefs_button.set_tooltip_text("Settings")
579 prefs_button.set_relief(gtk.RELIEF_NONE)
581 self.status_bar.pack_end(prefs_button,False,False)
583 pw_icon = gtk.Image()
584 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
585 pw_icon.set_alignment(0.5, 0.5)
586 pw_icon.set_size_request(16,16 )
590 password_button = gtk.Button()
591 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
592 password_button.add(pw_icon)
593 password_button.set_relief(gtk.RELIEF_NONE)
594 password_button.show()
595 self.status_bar.pack_end(password_button,False,False)
597 self.window.add(vbox)
598 self.window.show_all()
601 self.context_id = self.status_bar.get_context_id("statusbar")
602 self.update_status_bar()
604 self.wallet.network.register_callback('updated', self.update_callback)
607 def update_status_bar_thread():
609 gobject.idle_add( self.update_status_bar )
613 def check_recipient_thread():
617 if self.payto_entry.is_focus():
619 r = self.payto_entry.get_text()
623 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
625 to_address = self.wallet.get_alias(r, interactive=False)
629 s = r + ' <' + to_address + '>'
630 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
633 thread.start_new_thread(update_status_bar_thread, ())
635 thread.start_new_thread(check_recipient_thread, ())
636 self.notebook.set_current_page(0)
638 def update_callback(self):
639 self.wallet_updated = True
642 def add_tab(self, page, name):
643 tab_label = gtk.Label(name)
645 self.notebook.append_page(page, tab_label)
648 def create_send_tab(self):
650 page = vbox = gtk.VBox()
654 payto_label = gtk.Label('Pay to:')
655 payto_label.set_size_request(100,-1)
656 payto.pack_start(payto_label, False)
657 payto_entry = gtk.Entry()
658 payto_entry.set_size_request(450, 26)
659 payto.pack_start(payto_entry, False)
660 vbox.pack_start(payto, False, False, 5)
663 message_label = gtk.Label('Description:')
664 message_label.set_size_request(100,-1)
665 message.pack_start(message_label, False)
666 message_entry = gtk.Entry()
667 message_entry.set_size_request(450, 26)
668 message.pack_start(message_entry, False)
669 vbox.pack_start(message, False, False, 5)
671 amount_box = gtk.HBox()
672 amount_label = gtk.Label('Amount:')
673 amount_label.set_size_request(100,-1)
674 amount_box.pack_start(amount_label, False)
675 amount_entry = gtk.Entry()
676 amount_entry.set_size_request(120, -1)
677 amount_box.pack_start(amount_entry, False)
678 vbox.pack_start(amount_box, False, False, 5)
680 self.fee_box = fee_box = gtk.HBox()
681 fee_label = gtk.Label('Fee:')
682 fee_label.set_size_request(100,-1)
683 fee_box.pack_start(fee_label, False)
684 fee_entry = gtk.Entry()
685 fee_entry.set_size_request(60, 26)
686 fee_box.pack_start(fee_entry, False)
687 vbox.pack_start(fee_box, False, False, 5)
690 empty_label = gtk.Label('')
691 empty_label.set_size_request(100,-1)
692 end_box.pack_start(empty_label, False)
693 send_button = gtk.Button("Send")
695 end_box.pack_start(send_button, False, False, 0)
696 clear_button = gtk.Button("Clear")
698 end_box.pack_start(clear_button, False, False, 15)
699 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
700 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
702 vbox.pack_start(end_box, False, False, 5)
704 # display this line only if there is a signature
705 payto_sig = gtk.HBox()
706 payto_sig_id = gtk.Label('')
707 payto_sig.pack_start(payto_sig_id, False)
708 vbox.pack_start(payto_sig, True, True, 5)
711 self.user_fee = False
713 def entry_changed( entry, is_fee ):
714 self.funds_error = False
715 amount = numbify(amount_entry)
716 fee = numbify(fee_entry)
717 if not is_fee: fee = None
720 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
722 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
725 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
726 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
727 send_button.set_sensitive(True)
729 send_button.set_sensitive(False)
730 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
731 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
732 self.funds_error = True
734 amount_entry.connect('changed', entry_changed, False)
735 fee_entry.connect('changed', entry_changed, True)
737 self.payto_entry = payto_entry
738 self.payto_fee_entry = fee_entry
739 self.payto_sig_id = payto_sig_id
740 self.payto_sig = payto_sig
741 self.amount_entry = amount_entry
742 self.message_entry = message_entry
743 self.add_tab(page, 'Send')
745 def set_frozen(self,entry,frozen):
747 entry.set_editable(False)
748 entry.set_has_frame(False)
749 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
751 entry.set_editable(True)
752 entry.set_has_frame(True)
753 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
755 def set_url(self, url):
756 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
757 self.notebook.set_current_page(1)
758 self.payto_entry.set_text(payto)
759 self.message_entry.set_text(message)
760 self.amount_entry.set_text(amount)
762 self.set_frozen(self.payto_entry,True)
763 self.set_frozen(self.amount_entry,True)
764 self.set_frozen(self.message_entry,True)
765 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
767 self.payto_sig.set_visible(False)
769 def create_about_tab(self):
774 tv.set_editable(False)
775 tv.set_cursor_visible(False)
776 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
777 scroll = gtk.ScrolledWindow()
779 page.pack_start(scroll)
780 self.info = tv.get_buffer()
781 self.add_tab(page, 'Wall')
783 def do_clear(self, w, data):
784 self.payto_sig.set_visible(False)
785 self.payto_fee_entry.set_text('')
786 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
787 self.set_frozen(entry,False)
790 def question(self,msg):
791 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
793 result = dialog.run()
795 return result == gtk.RESPONSE_OK
797 def do_send(self, w, data):
798 payto_entry, label_entry, amount_entry, fee_entry = data
799 label = label_entry.get_text()
800 r = payto_entry.get_text()
803 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
804 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
807 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
811 self.update_sending_tab()
814 to_address = m2.group(5)
818 if not is_valid(to_address):
819 self.show_message( "invalid bitcoin address:\n"+to_address)
823 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
825 self.show_message( "invalid amount")
828 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
830 self.show_message( "invalid fee")
833 if self.wallet.use_encryption:
834 password = password_dialog(self.window)
841 tx = self.wallet.mktx( [(to_address, amount)], password, fee )
842 except BaseException, e:
843 self.show_message(str(e))
846 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
847 self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." )
852 self.wallet.labels[tx.hash()] = label
854 status, msg = self.wallet.sendtx( tx )
856 self.show_message( "payment sent.\n" + msg )
857 payto_entry.set_text("")
858 label_entry.set_text("")
859 amount_entry.set_text("")
860 fee_entry.set_text("")
862 self.update_sending_tab()
864 self.show_message( msg )
867 def treeview_button_press(self, treeview, event):
868 if event.type == gtk.gdk._2BUTTON_PRESS:
869 c = treeview.get_cursor()[0]
870 if treeview == self.history_treeview:
871 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
872 self.show_message(tx_details)
873 elif treeview == self.contacts_treeview:
874 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
875 #a = self.wallet.aliases.get(m)
877 # if a[0] in self.wallet.authorities.keys():
878 # s = self.wallet.authorities.get(a[0])
881 # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
882 # self.show_message(msg)
885 def treeview_key_press(self, treeview, event):
886 c = treeview.get_cursor()[0]
887 if event.keyval == gtk.keysyms.Up:
889 treeview.parent.grab_focus()
890 treeview.set_cursor((0,))
891 elif event.keyval == gtk.keysyms.Return:
892 if treeview == self.history_treeview:
893 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
894 self.show_message(tx_details)
895 elif treeview == self.contacts_treeview:
896 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
897 #a = self.wallet.aliases.get(m)
899 # if a[0] in self.wallet.authorities.keys():
900 # s = self.wallet.authorities.get(a[0])
903 # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
904 # self.show_message(msg)
908 def create_history_tab(self):
910 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
911 treeview = gtk.TreeView(model=self.history_list)
912 self.history_treeview = treeview
913 treeview.set_tooltip_column(7)
915 treeview.connect('key-press-event', self.treeview_key_press)
916 treeview.connect('button-press-event', self.treeview_button_press)
918 tvcolumn = gtk.TreeViewColumn('')
919 treeview.append_column(tvcolumn)
920 cell = gtk.CellRendererPixbuf()
921 tvcolumn.pack_start(cell, False)
922 tvcolumn.set_attributes(cell, stock_id=1)
924 tvcolumn = gtk.TreeViewColumn('Date')
925 treeview.append_column(tvcolumn)
926 cell = gtk.CellRendererText()
927 tvcolumn.pack_start(cell, False)
928 tvcolumn.add_attribute(cell, 'text', 2)
930 tvcolumn = gtk.TreeViewColumn('Description')
931 treeview.append_column(tvcolumn)
932 cell = gtk.CellRendererText()
933 cell.set_property('foreground', 'grey')
934 cell.set_property('family', MONOSPACE_FONT)
935 cell.set_property('editable', True)
936 def edited_cb(cell, path, new_text, h_list):
937 tx = h_list.get_value( h_list.get_iter(path), 0)
938 self.wallet.labels[tx] = new_text
940 self.update_history_tab()
941 cell.connect('edited', edited_cb, self.history_list)
942 def editing_started(cell, entry, path, h_list):
943 tx = h_list.get_value( h_list.get_iter(path), 0)
944 if not self.wallet.labels.get(tx): entry.set_text('')
945 cell.connect('editing-started', editing_started, self.history_list)
946 tvcolumn.set_expand(True)
947 tvcolumn.pack_start(cell, True)
948 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
950 tvcolumn = gtk.TreeViewColumn('Amount')
951 treeview.append_column(tvcolumn)
952 cell = gtk.CellRendererText()
953 cell.set_alignment(1, 0.5)
954 cell.set_property('family', MONOSPACE_FONT)
955 tvcolumn.pack_start(cell, False)
956 tvcolumn.add_attribute(cell, 'text', 5)
958 tvcolumn = gtk.TreeViewColumn('Balance')
959 treeview.append_column(tvcolumn)
960 cell = gtk.CellRendererText()
961 cell.set_alignment(1, 0.5)
962 cell.set_property('family', MONOSPACE_FONT)
963 tvcolumn.pack_start(cell, False)
964 tvcolumn.add_attribute(cell, 'text', 6)
966 tvcolumn = gtk.TreeViewColumn('Tooltip')
967 treeview.append_column(tvcolumn)
968 cell = gtk.CellRendererText()
969 tvcolumn.pack_start(cell, False)
970 tvcolumn.add_attribute(cell, 'text', 7)
971 tvcolumn.set_visible(False)
973 scroll = gtk.ScrolledWindow()
974 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
977 self.add_tab(scroll, 'History')
978 self.update_history_tab()
981 def create_recv_tab(self):
982 self.recv_list = gtk.ListStore(str, str, str)
983 self.add_tab( self.make_address_list(True), 'Receive')
984 self.update_receiving_tab()
986 def create_book_tab(self):
987 self.addressbook_list = gtk.ListStore(str, str, str)
988 self.add_tab( self.make_address_list(False), 'Contacts')
989 self.update_sending_tab()
991 def make_address_list(self, is_recv):
992 liststore = self.recv_list if is_recv else self.addressbook_list
993 treeview = gtk.TreeView(model= liststore)
994 treeview.connect('key-press-event', self.treeview_key_press)
995 treeview.connect('button-press-event', self.treeview_button_press)
998 self.contacts_treeview = treeview
1000 tvcolumn = gtk.TreeViewColumn('Address')
1001 treeview.append_column(tvcolumn)
1002 cell = gtk.CellRendererText()
1003 cell.set_property('family', MONOSPACE_FONT)
1004 tvcolumn.pack_start(cell, True)
1005 tvcolumn.add_attribute(cell, 'text', 0)
1007 tvcolumn = gtk.TreeViewColumn('Label')
1008 tvcolumn.set_expand(True)
1009 treeview.append_column(tvcolumn)
1010 cell = gtk.CellRendererText()
1011 cell.set_property('editable', True)
1012 def edited_cb2(cell, path, new_text, liststore):
1013 address = liststore.get_value( liststore.get_iter(path), 0)
1014 self.wallet.labels[address] = new_text
1016 self.update_receiving_tab()
1017 self.update_sending_tab()
1018 self.update_history_tab()
1019 cell.connect('edited', edited_cb2, liststore)
1020 tvcolumn.pack_start(cell, True)
1021 tvcolumn.add_attribute(cell, 'text', 1)
1023 tvcolumn = gtk.TreeViewColumn('Tx')
1024 treeview.append_column(tvcolumn)
1025 cell = gtk.CellRendererText()
1026 tvcolumn.pack_start(cell, True)
1027 tvcolumn.add_attribute(cell, 'text', 2)
1029 scroll = gtk.ScrolledWindow()
1030 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1031 scroll.add(treeview)
1035 button = gtk.Button("New")
1036 button.connect("clicked", self.newaddress_dialog)
1038 hbox.pack_start(button,False)
1040 def showqrcode(w, treeview, liststore):
1041 path, col = treeview.get_cursor()
1043 address = liststore.get_value(liststore.get_iter(path), 0)
1044 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1048 size = qr.getModuleCount()*boxsize
1049 def area_expose_cb(area, event):
1050 style = area.get_style()
1051 k = qr.getModuleCount()
1054 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1055 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1056 area = gtk.DrawingArea()
1057 area.set_size_request(size, size)
1058 area.connect("expose-event", area_expose_cb)
1060 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1061 dialog.vbox.add(area)
1065 button = gtk.Button("QR")
1066 button.connect("clicked", showqrcode, treeview, liststore)
1068 hbox.pack_start(button,False)
1070 button = gtk.Button("Copy to clipboard")
1071 def copy2clipboard(w, treeview, liststore):
1073 path, col = treeview.get_cursor()
1075 address = liststore.get_value( liststore.get_iter(path), 0)
1076 if platform.system() == 'Windows':
1077 from Tkinter import Tk
1081 r.clipboard_append( address )
1084 c = gtk.clipboard_get()
1085 c.set_text( address )
1086 button.connect("clicked", copy2clipboard, treeview, liststore)
1088 hbox.pack_start(button,False)
1091 button = gtk.Button("Pay to")
1092 def payto(w, treeview, liststore):
1093 path, col = treeview.get_cursor()
1095 address = liststore.get_value( liststore.get_iter(path), 0)
1096 self.payto_entry.set_text( address )
1097 self.notebook.set_current_page(1)
1098 self.amount_entry.grab_focus()
1100 button.connect("clicked", payto, treeview, liststore)
1102 hbox.pack_start(button,False)
1105 vbox.pack_start(scroll,True)
1106 vbox.pack_start(hbox, False)
1109 def update_status_bar(self):
1110 interface = self.wallet.network.interface
1111 if self.funds_error:
1112 text = "Not enough funds"
1113 elif interface and interface.is_connected:
1114 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.network.blockchain.height))
1115 if not self.wallet.up_to_date:
1116 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1117 text = "Synchronizing..."
1119 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1120 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.network.blockchain.height))
1121 c, u = self.wallet.get_balance()
1122 text = "Balance: %s "%( format_satoshis(c,False,self.num_zeros) )
1123 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.num_zeros).strip() )
1125 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1126 self.network_button.set_tooltip_text("Not connected.")
1127 text = "Not connected"
1129 self.status_bar.pop(self.context_id)
1130 self.status_bar.push(self.context_id, text)
1132 if self.wallet.up_to_date and self.wallet_updated:
1133 self.update_history_tab()
1134 self.update_receiving_tab()
1135 # addressbook too...
1136 self.info.set_text( self.wallet.network.banner )
1137 self.wallet_updated = False
1139 def update_receiving_tab(self):
1140 self.recv_list.clear()
1141 for address in self.wallet.addresses(True):
1142 if self.wallet.is_change(address):continue
1143 label = self.wallet.labels.get(address)
1144 h = self.wallet.history.get(address,[])
1146 tx = "None" if n==0 else "%d"%n
1147 self.recv_list.append((address, label, tx ))
1149 def update_sending_tab(self):
1150 # detect addresses that are not mine in history, add them here...
1151 self.addressbook_list.clear()
1152 #for alias, v in self.wallet.aliases.items():
1154 # label = self.wallet.labels.get(alias)
1155 # self.addressbook_list.append((alias, label, '-'))
1157 for address in self.wallet.addressbook:
1158 label = self.wallet.labels.get(address)
1159 n = self.wallet.get_num_tx(address)
1160 self.addressbook_list.append((address, label, "%d"%n))
1162 def update_history_tab(self):
1163 cursor = self.history_treeview.get_cursor()[0]
1164 self.history_list.clear()
1166 for item in self.wallet.get_tx_history():
1167 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1170 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1173 conf_icon = gtk.STOCK_APPLY
1175 time_str = 'unverified'
1178 time_str = 'pending'
1179 conf_icon = gtk.STOCK_EXECUTE
1181 label, is_default_label = self.wallet.get_label(tx_hash)
1182 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1183 details = self.get_tx_details(tx_hash)
1185 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1186 format_satoshis(value,True,self.num_zeros, whitespaces=True),
1187 format_satoshis(balance,False,self.num_zeros, whitespaces=True), tooltip, details] )
1188 if cursor: self.history_treeview.set_cursor( cursor )
1191 def get_tx_details(self, tx_hash):
1193 if not tx_hash: return ''
1194 tx = self.wallet.transactions.get(tx_hash)
1195 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
1196 conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1199 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1201 time_str = 'pending'
1203 inputs = map(lambda x: x.get('address'), tx.inputs)
1204 outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1205 tx_details = "Transaction Details" +"\n\n" \
1206 + "Transaction ID:\n" + tx_hash + "\n\n" \
1207 + "Status: %d confirmations\n"%conf
1210 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1211 + "Transaction fee: %s\n"% format_satoshis(fee, False)
1213 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1214 + "Transaction fee: unknown\n"
1216 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1218 tx_details += "Date: %s\n\n"%time_str \
1219 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1220 + "Outputs:\n-"+ '\n-'.join(outputs)
1226 def newaddress_dialog(self, w):
1228 title = "New Contact"
1229 dialog = gtk.Dialog(title, parent=self.window,
1230 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1231 buttons= ("cancel", 0, "ok",1) )
1235 label_label = gtk.Label('Label:')
1236 label_label.set_size_request(120,10)
1238 label.pack_start(label_label)
1239 label_entry = gtk.Entry()
1241 label.pack_start(label_entry)
1243 dialog.vbox.pack_start(label, False, True, 5)
1245 address = gtk.HBox()
1246 address_label = gtk.Label('Address:')
1247 address_label.set_size_request(120,10)
1248 address_label.show()
1249 address.pack_start(address_label)
1250 address_entry = gtk.Entry()
1251 address_entry.show()
1252 address.pack_start(address_entry)
1254 dialog.vbox.pack_start(address, False, True, 5)
1256 result = dialog.run()
1257 address = address_entry.get_text()
1258 label = label_entry.get_text()
1262 if is_valid(address):
1263 self.wallet.add_contact(address,label)
1264 self.update_sending_tab()
1266 errorDialog = gtk.MessageDialog(
1268 flags=gtk.DIALOG_MODAL,
1269 buttons= gtk.BUTTONS_CLOSE,
1270 message_format = "Invalid address")
1273 errorDialog.destroy()
1277 class ElectrumGui():
1279 def __init__(self, config, network):
1280 self.network = network
1281 self.config = config
1282 storage = WalletStorage(config)
1283 if not storage.file_exists:
1284 print "Wallet not found. try 'electrum create'"
1287 self.wallet = Wallet(storage)
1288 self.wallet.start_threads(network)
1291 def main(self, url=None):
1292 ew = ElectrumWindow(self.wallet, self.config)
1293 if url: ew.set_url(url)
1296 def restore_or_create(self):
1297 return restore_create_dialog(self.wallet)
1299 def seed_dialog(self):
1300 return run_recovery_dialog( self.wallet )
1302 def verify_seed(self):
1303 self.wallet.save_seed()
1306 def network_dialog(self):
1307 return run_network_dialog( self.wallet, parent=None )
1309 def show_seed(self):
1310 show_seed_dialog(self.wallet, None, None)
1312 def password_dialog(self):
1313 change_password_dialog(self.wallet, None, None)
1315 def restore_wallet(self):
1316 wallet = self.wallet
1318 dialog = gtk.MessageDialog(
1320 flags = gtk.DIALOG_MODAL,
1321 buttons = gtk.BUTTONS_CANCEL,
1322 message_format = "Please wait..." )
1325 def recover_thread( wallet, dialog ):
1326 while not wallet.is_up_to_date():
1328 gobject.idle_add( dialog.destroy )
1330 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1333 if r==gtk.RESPONSE_CANCEL: return False
1334 if not wallet.is_found():
1335 show_message("No transactions found for this seed")