3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import thread, time, ast, sys, re
21 import socket, traceback
25 from decimal import Decimal
26 from util import print_error
28 import pyqrnative, mnemonic
30 gtk.gdk.threads_init()
33 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
35 from wallet import format_satoshis
36 from interface import DEFAULT_SERVERS
38 def numbify(entry, is_int = False):
39 text = entry.get_text().strip()
41 if not is_int: chars +='.'
42 s = ''.join([i for i in text if i in chars])
47 s = s[:p] + '.' + s[p:p+8]
49 amount = int( Decimal(s) * 100000000 )
63 def show_seed_dialog(wallet, password, parent):
65 show_message("No seed")
68 seed = wallet.pw_decode( wallet.seed, password)
70 show_message("Incorrect password")
72 dialog = gtk.MessageDialog(
74 flags = gtk.DIALOG_MODAL,
75 buttons = gtk.BUTTONS_OK,
76 message_format = "Your wallet generation seed is:\n\n" + seed \
77 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
78 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
79 dialog.set_title("Seed")
84 def restore_create_dialog(wallet):
86 # ask if the user wants to create a new wallet, or recover from a seed.
87 # if he wants to recover, and nothing is found, do not create wallet
88 dialog = gtk.Dialog("electrum", parent=None,
89 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
90 buttons= ("create", 0, "restore",1, "cancel",2) )
92 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
94 dialog.vbox.pack_start(label)
100 return 'restore' if r==1 else 'create'
104 def run_recovery_dialog(wallet):
105 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
106 dialog = gtk.MessageDialog(
108 flags = gtk.DIALOG_MODAL,
109 buttons = gtk.BUTTONS_OK_CANCEL,
110 message_format = message)
113 dialog.set_default_response(gtk.RESPONSE_OK)
115 # ask seed, server and gap in the same dialog
116 seed_box = gtk.HBox()
117 seed_label = gtk.Label('Seed or mnemonic:')
118 seed_label.set_size_request(150,-1)
119 seed_box.pack_start(seed_label, False, False, 10)
121 seed_entry = gtk.Entry()
123 seed_entry.set_size_request(450,-1)
124 seed_box.pack_start(seed_entry, False, False, 10)
125 add_help_button(seed_box, '.')
127 vbox.pack_start(seed_box, False, False, 5)
130 gap_label = gtk.Label('Gap limit:')
131 gap_label.set_size_request(150,10)
133 gap.pack_start(gap_label,False, False, 10)
134 gap_entry = gtk.Entry()
135 gap_entry.set_text("%d"%wallet.gap_limit)
136 gap_entry.connect('changed', numbify, True)
138 gap.pack_start(gap_entry,False,False, 10)
139 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.')
141 vbox.pack_start(gap, False,False, 5)
145 gap = gap_entry.get_text()
146 seed = seed_entry.get_text()
149 if r==gtk.RESPONSE_CANCEL:
155 show_message("error")
161 print_error("Warning: Not hex, trying decode")
162 seed = mnemonic.mn_decode( seed.split(' ') )
164 show_message("no seed")
168 wallet.gap_limit = gap
174 def run_settings_dialog(wallet, parent):
176 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
178 dialog = gtk.MessageDialog(
180 flags = gtk.DIALOG_MODAL,
181 buttons = gtk.BUTTONS_OK_CANCEL,
182 message_format = message)
185 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
187 dialog.set_image(image)
188 dialog.set_title("Settings")
191 dialog.set_default_response(gtk.RESPONSE_OK)
194 fee_entry = gtk.Entry()
195 fee_label = gtk.Label('Transaction fee:')
196 fee_label.set_size_request(150,10)
198 fee.pack_start(fee_label,False, False, 10)
199 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
200 fee_entry.connect('changed', numbify, False)
202 fee.pack_start(fee_entry,False,False, 10)
203 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
205 vbox.pack_start(fee, False,False, 5)
208 nz_entry = gtk.Entry()
209 nz_label = gtk.Label('Display zeros:')
210 nz_label.set_size_request(150,10)
212 nz.pack_start(nz_label,False, False, 10)
213 nz_entry.set_text( str( wallet.num_zeros ))
214 nz_entry.connect('changed', numbify, True)
216 nz.pack_start(nz_entry,False,False, 10)
217 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'")
219 vbox.pack_start(nz, False,False, 5)
223 gui_label = gtk.Label('Default GUI:')
224 gui_label.set_size_request(150,10)
226 gui_box.pack_start(gui_label,False, False, 10)
227 gui_combo = gtk.combo_box_new_text()
228 gui_names = ['lite', 'classic', 'gtk', 'text']
229 for name in gui_names: gui_combo.append_text(name.capitalize())
231 gui_box.pack_start(gui_combo,False, False, 10)
232 gui_combo.set_active( gui_names.index( wallet.config.get("gui","lite")) )
234 add_help_button(gui_box, "Select which GUI mode to use at start up.")
236 vbox.pack_start(gui_box, False,False, 5)
240 fee = fee_entry.get_text()
241 nz = nz_entry.get_text()
242 gui = gui_names[ gui_combo.get_active()]
245 if r==gtk.RESPONSE_CANCEL:
249 fee = int( 100000000 * Decimal(fee) )
251 show_message("error")
253 if wallet.fee != fee:
261 show_message("error")
263 if wallet.num_zeros != nz:
264 wallet.num_zeros = nz
267 wallet.config.set_key('gui',gui,True)
272 def run_network_dialog( wallet, parent ):
274 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
275 interface = wallet.interface
277 if interface.is_connected:
278 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.verifier.height)
280 status = "Not connected"
283 status = "Please choose a server.\nSelect cancel if you are offline."
285 server = interface.server
286 plist, servers_list = interface.get_servers_list()
288 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
289 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
290 dialog.set_title("Server")
291 dialog.set_image(image)
295 host_box = gtk.HBox()
296 host_label = gtk.Label('Connect to:')
297 host_label.set_size_request(100,-1)
299 host_box.pack_start(host_label, False, False, 10)
300 host_entry = gtk.Entry()
301 host_entry.set_size_request(200,-1)
302 host_entry.set_text(server)
304 host_box.pack_start(host_entry, False, False, 10)
305 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)')
309 p_box = gtk.HBox(False, 10)
312 p_label = gtk.Label('Protocol:')
313 p_label.set_size_request(100,-1)
315 p_box.pack_start(p_label, False, False, 10)
317 radio1 = gtk.RadioButton(None, "tcp")
318 p_box.pack_start(radio1, True, True, 0)
320 radio2 = gtk.RadioButton(radio1, "http")
321 p_box.pack_start(radio2, True, True, 0)
325 return unicode(host_entry.get_text()).split(':')
327 def set_button(protocol):
330 elif protocol == 'h':
333 def set_protocol(protocol):
334 host = current_line()[0]
336 if protocol not in pp.keys():
337 protocol = pp.keys()[0]
340 host_entry.set_text( host + ':' + port + ':' + protocol)
342 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
343 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
345 server_list = gtk.ListStore(str)
346 for host in plist.keys():
347 server_list.append([host])
349 treeview = gtk.TreeView(model=server_list)
352 if wallet.interface.servers:
353 label = 'Active Servers'
355 label = 'Default Servers'
357 tvcolumn = gtk.TreeViewColumn(label)
358 treeview.append_column(tvcolumn)
359 cell = gtk.CellRendererText()
360 tvcolumn.pack_start(cell, False)
361 tvcolumn.add_attribute(cell, 'text', 0)
363 scroll = gtk.ScrolledWindow()
364 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
368 vbox.pack_start(host_box, False,False, 5)
369 vbox.pack_start(p_box, True, True, 0)
370 vbox.pack_start(scroll)
372 def my_treeview_cb(treeview):
373 path, view_column = treeview.get_cursor()
374 host = server_list.get_value( server_list.get_iter(path), 0)
380 protocol = pp.keys()[0]
382 host_entry.set_text( host + ':' + port + ':' + protocol)
385 treeview.connect('cursor-changed', my_treeview_cb)
389 server = host_entry.get_text()
392 if r==gtk.RESPONSE_CANCEL:
396 interface.set_server(server)
398 show_message("error:" + server)
402 wallet.config.set_key("server", server, True)
407 def show_message(message, parent=None):
408 dialog = gtk.MessageDialog(
410 flags = gtk.DIALOG_MODAL,
411 buttons = gtk.BUTTONS_CLOSE,
412 message_format = message )
417 def password_line(label):
418 password = gtk.HBox()
419 password_label = gtk.Label(label)
420 password_label.set_size_request(120,10)
421 password_label.show()
422 password.pack_start(password_label,False, False, 10)
423 password_entry = gtk.Entry()
424 password_entry.set_size_request(300,-1)
425 password_entry.set_visibility(False)
426 password_entry.show()
427 password.pack_start(password_entry,False,False, 10)
429 return password, password_entry
431 def password_dialog(parent):
432 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
433 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
434 dialog.get_image().set_visible(False)
435 current_pw, current_pw_entry = password_line('Password:')
436 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
437 dialog.vbox.pack_start(current_pw, False, True, 0)
439 result = dialog.run()
440 pw = current_pw_entry.get_text()
442 if result != gtk.RESPONSE_CANCEL: return pw
444 def change_password_dialog(wallet, parent, icon):
446 show_message("No seed")
450 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'
452 msg = "Please choose a password to encrypt your wallet keys"
454 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
455 dialog.set_title("Change password")
457 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
459 dialog.set_image(image)
461 if wallet.use_encryption:
462 current_pw, current_pw_entry = password_line('Current password:')
463 dialog.vbox.pack_start(current_pw, False, True, 0)
465 password, password_entry = password_line('New password:')
466 dialog.vbox.pack_start(password, False, True, 5)
467 password2, password2_entry = password_line('Confirm password:')
468 dialog.vbox.pack_start(password2, False, True, 5)
471 result = dialog.run()
472 password = current_pw_entry.get_text() if wallet.use_encryption else None
473 new_password = password_entry.get_text()
474 new_password2 = password2_entry.get_text()
476 if result == gtk.RESPONSE_CANCEL:
480 seed = wallet.pw_decode( wallet.seed, password)
482 show_message("Incorrect password")
485 if new_password != new_password2:
486 show_message("passwords do not match")
489 wallet.update_password(seed, password, new_password)
492 if wallet.use_encryption:
493 icon.set_tooltip_text('wallet is encrypted')
495 icon.set_tooltip_text('wallet is unencrypted')
498 def add_help_button(hbox, message):
499 button = gtk.Button('?')
500 button.connect("clicked", lambda x: show_message(message))
502 hbox.pack_start(button,False, False)
505 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
507 gobject.type_register(MyWindow)
508 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
509 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
512 class ElectrumWindow:
514 def show_message(self, msg):
515 show_message(msg, self.window)
517 def __init__(self, wallet, config):
520 self.funds_error = False # True if not enough funds
522 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
523 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
524 if not self.wallet.seed: title += ' [seedless]'
525 self.window.set_title(title)
526 self.window.connect("destroy", gtk.main_quit)
527 self.window.set_border_width(0)
528 self.window.connect('mykeypress', gtk.main_quit)
529 self.window.set_default_size(720, 350)
530 self.wallet_updated = False
534 self.notebook = gtk.Notebook()
535 self.create_history_tab()
537 self.create_send_tab()
538 self.create_recv_tab()
539 self.create_book_tab()
540 self.create_about_tab()
542 vbox.pack_start(self.notebook, True, True, 2)
544 self.status_bar = gtk.Statusbar()
545 vbox.pack_start(self.status_bar, False, False, 0)
547 self.status_image = gtk.Image()
548 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
549 self.status_image.set_alignment(True, 0.5 )
550 self.status_image.show()
552 self.network_button = gtk.Button()
553 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
554 self.network_button.add(self.status_image)
555 self.network_button.set_relief(gtk.RELIEF_NONE)
556 self.network_button.show()
557 self.status_bar.pack_end(self.network_button, False, False)
560 def seedb(w, wallet):
561 if wallet.use_encryption:
562 password = password_dialog(self.window)
563 if not password: return
564 else: password = None
565 show_seed_dialog(wallet, password, self.window)
566 button = gtk.Button('S')
567 button.connect("clicked", seedb, wallet )
568 button.set_relief(gtk.RELIEF_NONE)
570 self.status_bar.pack_end(button,False, False)
572 settings_icon = gtk.Image()
573 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
574 settings_icon.set_alignment(0.5, 0.5)
575 settings_icon.set_size_request(16,16 )
578 prefs_button = gtk.Button()
579 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
580 prefs_button.add(settings_icon)
581 prefs_button.set_tooltip_text("Settings")
582 prefs_button.set_relief(gtk.RELIEF_NONE)
584 self.status_bar.pack_end(prefs_button,False,False)
586 pw_icon = gtk.Image()
587 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
588 pw_icon.set_alignment(0.5, 0.5)
589 pw_icon.set_size_request(16,16 )
593 password_button = gtk.Button()
594 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
595 password_button.add(pw_icon)
596 password_button.set_relief(gtk.RELIEF_NONE)
597 password_button.show()
598 self.status_bar.pack_end(password_button,False,False)
600 self.window.add(vbox)
601 self.window.show_all()
604 self.context_id = self.status_bar.get_context_id("statusbar")
605 self.update_status_bar()
607 self.wallet.interface.register_callback('updated', self.update_callback)
610 def update_status_bar_thread():
612 gobject.idle_add( self.update_status_bar )
616 def check_recipient_thread():
620 if self.payto_entry.is_focus():
622 r = self.payto_entry.get_text()
626 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
628 to_address = self.wallet.get_alias(r, interactive=False)
632 s = r + ' <' + to_address + '>'
633 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
636 thread.start_new_thread(update_status_bar_thread, ())
638 thread.start_new_thread(check_recipient_thread, ())
639 self.notebook.set_current_page(0)
641 def update_callback(self):
642 self.wallet_updated = True
645 def add_tab(self, page, name):
646 tab_label = gtk.Label(name)
648 self.notebook.append_page(page, tab_label)
651 def create_send_tab(self):
653 page = vbox = gtk.VBox()
657 payto_label = gtk.Label('Pay to:')
658 payto_label.set_size_request(100,-1)
659 payto.pack_start(payto_label, False)
660 payto_entry = gtk.Entry()
661 payto_entry.set_size_request(450, 26)
662 payto.pack_start(payto_entry, False)
663 vbox.pack_start(payto, False, False, 5)
666 message_label = gtk.Label('Description:')
667 message_label.set_size_request(100,-1)
668 message.pack_start(message_label, False)
669 message_entry = gtk.Entry()
670 message_entry.set_size_request(450, 26)
671 message.pack_start(message_entry, False)
672 vbox.pack_start(message, False, False, 5)
674 amount_box = gtk.HBox()
675 amount_label = gtk.Label('Amount:')
676 amount_label.set_size_request(100,-1)
677 amount_box.pack_start(amount_label, False)
678 amount_entry = gtk.Entry()
679 amount_entry.set_size_request(120, -1)
680 amount_box.pack_start(amount_entry, False)
681 vbox.pack_start(amount_box, False, False, 5)
683 self.fee_box = fee_box = gtk.HBox()
684 fee_label = gtk.Label('Fee:')
685 fee_label.set_size_request(100,-1)
686 fee_box.pack_start(fee_label, False)
687 fee_entry = gtk.Entry()
688 fee_entry.set_size_request(60, 26)
689 fee_box.pack_start(fee_entry, False)
690 vbox.pack_start(fee_box, False, False, 5)
693 empty_label = gtk.Label('')
694 empty_label.set_size_request(100,-1)
695 end_box.pack_start(empty_label, False)
696 send_button = gtk.Button("Send")
698 end_box.pack_start(send_button, False, False, 0)
699 clear_button = gtk.Button("Clear")
701 end_box.pack_start(clear_button, False, False, 15)
702 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
703 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
705 vbox.pack_start(end_box, False, False, 5)
707 # display this line only if there is a signature
708 payto_sig = gtk.HBox()
709 payto_sig_id = gtk.Label('')
710 payto_sig.pack_start(payto_sig_id, False)
711 vbox.pack_start(payto_sig, True, True, 5)
714 self.user_fee = False
716 def entry_changed( entry, is_fee ):
717 self.funds_error = False
718 amount = numbify(amount_entry)
719 fee = numbify(fee_entry)
720 if not is_fee: fee = None
723 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
725 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
728 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
729 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
730 send_button.set_sensitive(True)
732 send_button.set_sensitive(False)
733 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
734 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
735 self.funds_error = True
737 amount_entry.connect('changed', entry_changed, False)
738 fee_entry.connect('changed', entry_changed, True)
740 self.payto_entry = payto_entry
741 self.payto_fee_entry = fee_entry
742 self.payto_sig_id = payto_sig_id
743 self.payto_sig = payto_sig
744 self.amount_entry = amount_entry
745 self.message_entry = message_entry
746 self.add_tab(page, 'Send')
748 def set_frozen(self,entry,frozen):
750 entry.set_editable(False)
751 entry.set_has_frame(False)
752 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
754 entry.set_editable(True)
755 entry.set_has_frame(True)
756 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
758 def set_url(self, url):
759 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
760 self.notebook.set_current_page(1)
761 self.payto_entry.set_text(payto)
762 self.message_entry.set_text(message)
763 self.amount_entry.set_text(amount)
765 self.set_frozen(self.payto_entry,True)
766 self.set_frozen(self.amount_entry,True)
767 self.set_frozen(self.message_entry,True)
768 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
770 self.payto_sig.set_visible(False)
772 def create_about_tab(self):
777 tv.set_editable(False)
778 tv.set_cursor_visible(False)
779 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
780 scroll = gtk.ScrolledWindow()
782 page.pack_start(scroll)
783 self.info = tv.get_buffer()
784 self.add_tab(page, 'Wall')
786 def do_clear(self, w, data):
787 self.payto_sig.set_visible(False)
788 self.payto_fee_entry.set_text('')
789 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
790 self.set_frozen(entry,False)
793 def question(self,msg):
794 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
796 result = dialog.run()
798 return result == gtk.RESPONSE_OK
800 def do_send(self, w, data):
801 payto_entry, label_entry, amount_entry, fee_entry = data
802 label = label_entry.get_text()
803 r = payto_entry.get_text()
806 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
807 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
810 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
814 self.update_sending_tab()
817 to_address = m2.group(5)
821 if not self.wallet.is_valid(to_address):
822 self.show_message( "invalid bitcoin address:\n"+to_address)
826 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
828 self.show_message( "invalid amount")
831 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
833 self.show_message( "invalid fee")
836 if self.wallet.use_encryption:
837 password = password_dialog(self.window)
844 tx = self.wallet.mktx( to_address, amount, label, password, fee )
845 except BaseException, e:
846 self.show_message(str(e))
849 status, msg = self.wallet.sendtx( tx )
851 self.show_message( "payment sent.\n" + msg )
852 payto_entry.set_text("")
853 label_entry.set_text("")
854 amount_entry.set_text("")
855 fee_entry.set_text("")
857 self.update_sending_tab()
859 self.show_message( msg )
862 def treeview_button_press(self, treeview, event):
863 if event.type == gtk.gdk._2BUTTON_PRESS:
864 c = treeview.get_cursor()[0]
865 if treeview == self.history_treeview:
866 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
867 self.show_message(tx_details)
868 elif treeview == self.contacts_treeview:
869 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
870 a = self.wallet.aliases.get(m)
872 if a[0] in self.wallet.authorities.keys():
873 s = self.wallet.authorities.get(a[0])
876 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
877 self.show_message(msg)
880 def treeview_key_press(self, treeview, event):
881 c = treeview.get_cursor()[0]
882 if event.keyval == gtk.keysyms.Up:
884 treeview.parent.grab_focus()
885 treeview.set_cursor((0,))
886 elif event.keyval == gtk.keysyms.Return:
887 if treeview == self.history_treeview:
888 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
889 self.show_message(tx_details)
890 elif treeview == self.contacts_treeview:
891 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
892 a = self.wallet.aliases.get(m)
894 if a[0] in self.wallet.authorities.keys():
895 s = self.wallet.authorities.get(a[0])
898 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
899 self.show_message(msg)
903 def create_history_tab(self):
905 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
906 treeview = gtk.TreeView(model=self.history_list)
907 self.history_treeview = treeview
908 treeview.set_tooltip_column(7)
910 treeview.connect('key-press-event', self.treeview_key_press)
911 treeview.connect('button-press-event', self.treeview_button_press)
913 tvcolumn = gtk.TreeViewColumn('')
914 treeview.append_column(tvcolumn)
915 cell = gtk.CellRendererPixbuf()
916 tvcolumn.pack_start(cell, False)
917 tvcolumn.set_attributes(cell, stock_id=1)
919 tvcolumn = gtk.TreeViewColumn('Date')
920 treeview.append_column(tvcolumn)
921 cell = gtk.CellRendererText()
922 tvcolumn.pack_start(cell, False)
923 tvcolumn.add_attribute(cell, 'text', 2)
925 tvcolumn = gtk.TreeViewColumn('Description')
926 treeview.append_column(tvcolumn)
927 cell = gtk.CellRendererText()
928 cell.set_property('foreground', 'grey')
929 cell.set_property('family', MONOSPACE_FONT)
930 cell.set_property('editable', True)
931 def edited_cb(cell, path, new_text, h_list):
932 tx = h_list.get_value( h_list.get_iter(path), 0)
933 self.wallet.labels[tx] = new_text
935 self.update_history_tab()
936 cell.connect('edited', edited_cb, self.history_list)
937 def editing_started(cell, entry, path, h_list):
938 tx = h_list.get_value( h_list.get_iter(path), 0)
939 if not self.wallet.labels.get(tx): entry.set_text('')
940 cell.connect('editing-started', editing_started, self.history_list)
941 tvcolumn.set_expand(True)
942 tvcolumn.pack_start(cell, True)
943 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
945 tvcolumn = gtk.TreeViewColumn('Amount')
946 treeview.append_column(tvcolumn)
947 cell = gtk.CellRendererText()
948 cell.set_alignment(1, 0.5)
949 cell.set_property('family', MONOSPACE_FONT)
950 tvcolumn.pack_start(cell, False)
951 tvcolumn.add_attribute(cell, 'text', 5)
953 tvcolumn = gtk.TreeViewColumn('Balance')
954 treeview.append_column(tvcolumn)
955 cell = gtk.CellRendererText()
956 cell.set_alignment(1, 0.5)
957 cell.set_property('family', MONOSPACE_FONT)
958 tvcolumn.pack_start(cell, False)
959 tvcolumn.add_attribute(cell, 'text', 6)
961 tvcolumn = gtk.TreeViewColumn('Tooltip')
962 treeview.append_column(tvcolumn)
963 cell = gtk.CellRendererText()
964 tvcolumn.pack_start(cell, False)
965 tvcolumn.add_attribute(cell, 'text', 7)
966 tvcolumn.set_visible(False)
968 scroll = gtk.ScrolledWindow()
969 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
972 self.add_tab(scroll, 'History')
973 self.update_history_tab()
976 def create_recv_tab(self):
977 self.recv_list = gtk.ListStore(str, str, str)
978 self.add_tab( self.make_address_list(True), 'Receive')
979 self.update_receiving_tab()
981 def create_book_tab(self):
982 self.addressbook_list = gtk.ListStore(str, str, str)
983 self.add_tab( self.make_address_list(False), 'Contacts')
984 self.update_sending_tab()
986 def make_address_list(self, is_recv):
987 liststore = self.recv_list if is_recv else self.addressbook_list
988 treeview = gtk.TreeView(model= liststore)
989 treeview.connect('key-press-event', self.treeview_key_press)
990 treeview.connect('button-press-event', self.treeview_button_press)
993 self.contacts_treeview = treeview
995 tvcolumn = gtk.TreeViewColumn('Address')
996 treeview.append_column(tvcolumn)
997 cell = gtk.CellRendererText()
998 cell.set_property('family', MONOSPACE_FONT)
999 tvcolumn.pack_start(cell, True)
1000 tvcolumn.add_attribute(cell, 'text', 0)
1002 tvcolumn = gtk.TreeViewColumn('Label')
1003 tvcolumn.set_expand(True)
1004 treeview.append_column(tvcolumn)
1005 cell = gtk.CellRendererText()
1006 cell.set_property('editable', True)
1007 def edited_cb2(cell, path, new_text, liststore):
1008 address = liststore.get_value( liststore.get_iter(path), 0)
1009 self.wallet.labels[address] = new_text
1011 self.wallet.update_tx_labels()
1012 self.update_receiving_tab()
1013 self.update_sending_tab()
1014 self.update_history_tab()
1015 cell.connect('edited', edited_cb2, liststore)
1016 tvcolumn.pack_start(cell, True)
1017 tvcolumn.add_attribute(cell, 'text', 1)
1019 tvcolumn = gtk.TreeViewColumn('Tx')
1020 treeview.append_column(tvcolumn)
1021 cell = gtk.CellRendererText()
1022 tvcolumn.pack_start(cell, True)
1023 tvcolumn.add_attribute(cell, 'text', 2)
1025 scroll = gtk.ScrolledWindow()
1026 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1027 scroll.add(treeview)
1031 button = gtk.Button("New")
1032 button.connect("clicked", self.newaddress_dialog)
1034 hbox.pack_start(button,False)
1036 def showqrcode(w, treeview, liststore):
1037 path, col = treeview.get_cursor()
1039 address = liststore.get_value(liststore.get_iter(path), 0)
1040 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1044 size = qr.getModuleCount()*boxsize
1045 def area_expose_cb(area, event):
1046 style = area.get_style()
1047 k = qr.getModuleCount()
1050 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1051 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1052 area = gtk.DrawingArea()
1053 area.set_size_request(size, size)
1054 area.connect("expose-event", area_expose_cb)
1056 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1057 dialog.vbox.add(area)
1061 button = gtk.Button("QR")
1062 button.connect("clicked", showqrcode, treeview, liststore)
1064 hbox.pack_start(button,False)
1066 button = gtk.Button("Copy to clipboard")
1067 def copy2clipboard(w, treeview, liststore):
1069 path, col = treeview.get_cursor()
1071 address = liststore.get_value( liststore.get_iter(path), 0)
1072 if platform.system() == 'Windows':
1073 from Tkinter import Tk
1077 r.clipboard_append( address )
1080 c = gtk.clipboard_get()
1081 c.set_text( address )
1082 button.connect("clicked", copy2clipboard, treeview, liststore)
1084 hbox.pack_start(button,False)
1087 button = gtk.Button("Pay to")
1088 def payto(w, treeview, liststore):
1089 path, col = treeview.get_cursor()
1091 address = liststore.get_value( liststore.get_iter(path), 0)
1092 self.payto_entry.set_text( address )
1093 self.notebook.set_current_page(1)
1094 self.amount_entry.grab_focus()
1096 button.connect("clicked", payto, treeview, liststore)
1098 hbox.pack_start(button,False)
1101 vbox.pack_start(scroll,True)
1102 vbox.pack_start(hbox, False)
1105 def update_status_bar(self):
1106 interface = self.wallet.interface
1107 if self.funds_error:
1108 text = "Not enough funds"
1109 elif interface and interface.is_connected:
1110 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1111 if not self.wallet.up_to_date:
1112 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1113 text = "Synchronizing..."
1115 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1116 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1117 c, u = self.wallet.get_balance()
1118 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1119 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1121 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1122 self.network_button.set_tooltip_text("Not connected.")
1123 text = "Not connected"
1125 self.status_bar.pop(self.context_id)
1126 self.status_bar.push(self.context_id, text)
1128 if self.wallet.up_to_date and self.wallet_updated:
1129 self.update_history_tab()
1130 self.update_receiving_tab()
1131 # addressbook too...
1132 self.info.set_text( self.wallet.banner )
1133 self.wallet_updated = False
1135 def update_receiving_tab(self):
1136 self.recv_list.clear()
1137 for address in self.wallet.all_addresses():
1138 if self.wallet.is_change(address):continue
1139 label = self.wallet.labels.get(address)
1140 h = self.wallet.history.get(address,[])
1142 tx = "None" if n==0 else "%d"%n
1143 self.recv_list.append((address, label, tx ))
1145 def update_sending_tab(self):
1146 # detect addresses that are not mine in history, add them here...
1147 self.addressbook_list.clear()
1148 for alias, v in self.wallet.aliases.items():
1150 label = self.wallet.labels.get(alias)
1151 self.addressbook_list.append((alias, label, '-'))
1153 for address in self.wallet.addressbook:
1154 label = self.wallet.labels.get(address)
1156 for item in self.wallet.transactions.values():
1157 if address in item['outputs'] : n=n+1
1158 tx = "None" if n==0 else "%d"%n
1159 self.addressbook_list.append((address, label, tx))
1161 def update_history_tab(self):
1162 cursor = self.history_treeview.get_cursor()[0]
1163 self.history_list.clear()
1165 for item in self.wallet.get_tx_history():
1166 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1169 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1172 conf_icon = gtk.STOCK_APPLY
1174 time_str = 'pending'
1175 conf_icon = gtk.STOCK_EXECUTE
1177 label, is_default_label = self.wallet.get_label(tx_hash)
1178 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1179 details = self.wallet.get_tx_details(tx_hash)
1181 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1182 format_satoshis(value,True,self.wallet.num_zeros),
1183 format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1184 if cursor: self.history_treeview.set_cursor( cursor )
1188 def newaddress_dialog(self, w):
1190 title = "New Contact"
1191 dialog = gtk.Dialog(title, parent=self.window,
1192 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1193 buttons= ("cancel", 0, "ok",1) )
1197 label_label = gtk.Label('Label:')
1198 label_label.set_size_request(120,10)
1200 label.pack_start(label_label)
1201 label_entry = gtk.Entry()
1203 label.pack_start(label_entry)
1205 dialog.vbox.pack_start(label, False, True, 5)
1207 address = gtk.HBox()
1208 address_label = gtk.Label('Address:')
1209 address_label.set_size_request(120,10)
1210 address_label.show()
1211 address.pack_start(address_label)
1212 address_entry = gtk.Entry()
1213 address_entry.show()
1214 address.pack_start(address_entry)
1216 dialog.vbox.pack_start(address, False, True, 5)
1218 result = dialog.run()
1219 address = address_entry.get_text()
1220 label = label_entry.get_text()
1224 if self.wallet.is_valid(address):
1225 self.wallet.addressbook.append(address)
1226 if label: self.wallet.labels[address] = label
1228 self.update_sending_tab()
1230 errorDialog = gtk.MessageDialog(
1232 flags=gtk.DIALOG_MODAL,
1233 buttons= gtk.BUTTONS_CLOSE,
1234 message_format = "Invalid address")
1237 errorDialog.destroy()
1241 class ElectrumGui():
1243 def __init__(self, wallet, config):
1244 self.wallet = wallet
1245 self.config = config
1247 def main(self, url=None):
1248 ew = ElectrumWindow(self.wallet, self.config)
1249 if url: ew.set_url(url)
1252 def restore_or_create(self):
1253 return restore_create_dialog(self.wallet)
1255 def server_list_changed(self):
1258 def seed_dialog(self):
1259 # ask for seed and gap.
1260 return run_recovery_dialog( self.wallet )
1262 def network_dialog(self):
1263 return run_network_dialog( self.wallet, parent=None )
1265 def create_wallet(self):
1266 wallet = self.wallet
1267 wallet.new_seed(None)
1268 # generate first key
1269 wallet.init_mpk( wallet.seed )
1270 wallet.synchronize()
1271 #wallet.up_to_date_event.clear()
1273 # run a dialog indicating the seed, ask the user to remember it
1274 show_seed_dialog(wallet, None, None)
1276 change_password_dialog(wallet, None, None)
1278 def restore_wallet(self):
1279 wallet = self.wallet
1281 dialog = gtk.MessageDialog(
1283 flags = gtk.DIALOG_MODAL,
1284 buttons = gtk.BUTTONS_CANCEL,
1285 message_format = "Please wait..." )
1289 def recover_thread( wallet, dialog ):
1290 wallet.init_mpk( wallet.seed ) # not encrypted at this point
1291 wallet.up_to_date_event.clear()
1294 if wallet.is_found():
1295 # history and addressbook
1296 wallet.update_tx_history()
1297 wallet.fill_addressbook()
1298 print "Recovery successful"
1300 gobject.idle_add( dialog.destroy )
1302 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1305 if r==gtk.RESPONSE_CANCEL: return False
1306 if not wallet.is_found:
1307 show_message("No transactions found for this seed")