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.decode_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")
171 def run_settings_dialog(wallet, parent):
173 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
175 dialog = gtk.MessageDialog(
177 flags = gtk.DIALOG_MODAL,
178 buttons = gtk.BUTTONS_OK_CANCEL,
179 message_format = message)
182 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
184 dialog.set_image(image)
185 dialog.set_title("Settings")
188 dialog.set_default_response(gtk.RESPONSE_OK)
191 fee_entry = gtk.Entry()
192 fee_label = gtk.Label('Transaction fee:')
193 fee_label.set_size_request(150,10)
195 fee.pack_start(fee_label,False, False, 10)
196 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
197 fee_entry.connect('changed', numbify, False)
199 fee.pack_start(fee_entry,False,False, 10)
200 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
202 vbox.pack_start(fee, False,False, 5)
205 nz_entry = gtk.Entry()
206 nz_label = gtk.Label('Display zeros:')
207 nz_label.set_size_request(150,10)
209 nz.pack_start(nz_label,False, False, 10)
210 nz_entry.set_text( str( wallet.num_zeros ))
211 nz_entry.connect('changed', numbify, True)
213 nz.pack_start(nz_entry,False,False, 10)
214 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'")
216 vbox.pack_start(nz, False,False, 5)
220 gui_label = gtk.Label('Default GUI:')
221 gui_label.set_size_request(150,10)
223 gui_box.pack_start(gui_label,False, False, 10)
224 gui_combo = gtk.combo_box_new_text()
225 gui_names = ['lite', 'classic', 'gtk', 'text']
226 for name in gui_names: gui_combo.append_text(name.capitalize())
228 gui_box.pack_start(gui_combo,False, False, 10)
229 gui_combo.set_active( gui_names.index( wallet.config.get("gui","lite")) )
231 add_help_button(gui_box, "Select which GUI mode to use at start up.")
233 vbox.pack_start(gui_box, False,False, 5)
237 fee = fee_entry.get_text()
238 nz = nz_entry.get_text()
239 gui = gui_names[ gui_combo.get_active()]
242 if r==gtk.RESPONSE_CANCEL:
246 fee = int( 100000000 * Decimal(fee) )
248 show_message("error")
250 if wallet.fee != fee:
258 show_message("error")
260 if wallet.num_zeros != nz:
261 wallet.num_zeros = nz
264 wallet.config.set_key('gui',gui,True)
269 def run_network_dialog( wallet, parent ):
271 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
272 interface = wallet.interface
274 if interface.is_connected:
275 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.verifier.height)
277 status = "Not connected"
280 status = "Please choose a server.\nSelect cancel if you are offline."
282 server = interface.server
283 plist, servers_list = interface.get_servers_list()
285 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
286 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
287 dialog.set_title("Server")
288 dialog.set_image(image)
292 host_box = gtk.HBox()
293 host_label = gtk.Label('Connect to:')
294 host_label.set_size_request(100,-1)
296 host_box.pack_start(host_label, False, False, 10)
297 host_entry = gtk.Entry()
298 host_entry.set_size_request(200,-1)
299 host_entry.set_text(server)
301 host_box.pack_start(host_entry, False, False, 10)
302 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)')
306 p_box = gtk.HBox(False, 10)
309 p_label = gtk.Label('Protocol:')
310 p_label.set_size_request(100,-1)
312 p_box.pack_start(p_label, False, False, 10)
314 radio1 = gtk.RadioButton(None, "tcp")
315 p_box.pack_start(radio1, True, True, 0)
317 radio2 = gtk.RadioButton(radio1, "http")
318 p_box.pack_start(radio2, True, True, 0)
322 return unicode(host_entry.get_text()).split(':')
324 def set_button(protocol):
327 elif protocol == 'h':
330 def set_protocol(protocol):
331 host = current_line()[0]
333 if protocol not in pp.keys():
334 protocol = pp.keys()[0]
337 host_entry.set_text( host + ':' + port + ':' + protocol)
339 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
340 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
342 server_list = gtk.ListStore(str)
343 for host in plist.keys():
344 server_list.append([host])
346 treeview = gtk.TreeView(model=server_list)
349 if wallet.interface.servers:
350 label = 'Active Servers'
352 label = 'Default Servers'
354 tvcolumn = gtk.TreeViewColumn(label)
355 treeview.append_column(tvcolumn)
356 cell = gtk.CellRendererText()
357 tvcolumn.pack_start(cell, False)
358 tvcolumn.add_attribute(cell, 'text', 0)
360 scroll = gtk.ScrolledWindow()
361 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
365 vbox.pack_start(host_box, False,False, 5)
366 vbox.pack_start(p_box, True, True, 0)
367 vbox.pack_start(scroll)
369 def my_treeview_cb(treeview):
370 path, view_column = treeview.get_cursor()
371 host = server_list.get_value( server_list.get_iter(path), 0)
377 protocol = pp.keys()[0]
379 host_entry.set_text( host + ':' + port + ':' + protocol)
382 treeview.connect('cursor-changed', my_treeview_cb)
386 server = host_entry.get_text()
389 if r==gtk.RESPONSE_CANCEL:
393 interface.set_server(server)
395 show_message("error:" + server)
399 wallet.config.set_key("server", server, True)
404 def show_message(message, parent=None):
405 dialog = gtk.MessageDialog(
407 flags = gtk.DIALOG_MODAL,
408 buttons = gtk.BUTTONS_CLOSE,
409 message_format = message )
414 def password_line(label):
415 password = gtk.HBox()
416 password_label = gtk.Label(label)
417 password_label.set_size_request(120,10)
418 password_label.show()
419 password.pack_start(password_label,False, False, 10)
420 password_entry = gtk.Entry()
421 password_entry.set_size_request(300,-1)
422 password_entry.set_visibility(False)
423 password_entry.show()
424 password.pack_start(password_entry,False,False, 10)
426 return password, password_entry
428 def password_dialog(parent):
429 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
430 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
431 dialog.get_image().set_visible(False)
432 current_pw, current_pw_entry = password_line('Password:')
433 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
434 dialog.vbox.pack_start(current_pw, False, True, 0)
436 result = dialog.run()
437 pw = current_pw_entry.get_text()
439 if result != gtk.RESPONSE_CANCEL: return pw
441 def change_password_dialog(wallet, parent, icon):
443 show_message("No seed")
447 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'
449 msg = "Please choose a password to encrypt your wallet keys"
451 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
452 dialog.set_title("Change password")
454 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
456 dialog.set_image(image)
458 if wallet.use_encryption:
459 current_pw, current_pw_entry = password_line('Current password:')
460 dialog.vbox.pack_start(current_pw, False, True, 0)
462 password, password_entry = password_line('New password:')
463 dialog.vbox.pack_start(password, False, True, 5)
464 password2, password2_entry = password_line('Confirm password:')
465 dialog.vbox.pack_start(password2, False, True, 5)
468 result = dialog.run()
469 password = current_pw_entry.get_text() if wallet.use_encryption else None
470 new_password = password_entry.get_text()
471 new_password2 = password2_entry.get_text()
473 if result == gtk.RESPONSE_CANCEL:
477 seed = wallet.decode_seed(password)
479 show_message("Incorrect password")
482 if new_password != new_password2:
483 show_message("passwords do not match")
486 wallet.update_password(seed, password, new_password)
489 if wallet.use_encryption:
490 icon.set_tooltip_text('wallet is encrypted')
492 icon.set_tooltip_text('wallet is unencrypted')
495 def add_help_button(hbox, message):
496 button = gtk.Button('?')
497 button.connect("clicked", lambda x: show_message(message))
499 hbox.pack_start(button,False, False)
502 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
504 gobject.type_register(MyWindow)
505 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
506 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
509 class ElectrumWindow:
511 def show_message(self, msg):
512 show_message(msg, self.window)
514 def __init__(self, wallet, config):
517 self.funds_error = False # True if not enough funds
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.interface.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 self.wallet.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))
847 self.wallet.labels[tx.hash()] = label
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.update_receiving_tab()
1012 self.update_sending_tab()
1013 self.update_history_tab()
1014 cell.connect('edited', edited_cb2, liststore)
1015 tvcolumn.pack_start(cell, True)
1016 tvcolumn.add_attribute(cell, 'text', 1)
1018 tvcolumn = gtk.TreeViewColumn('Tx')
1019 treeview.append_column(tvcolumn)
1020 cell = gtk.CellRendererText()
1021 tvcolumn.pack_start(cell, True)
1022 tvcolumn.add_attribute(cell, 'text', 2)
1024 scroll = gtk.ScrolledWindow()
1025 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1026 scroll.add(treeview)
1030 button = gtk.Button("New")
1031 button.connect("clicked", self.newaddress_dialog)
1033 hbox.pack_start(button,False)
1035 def showqrcode(w, treeview, liststore):
1036 path, col = treeview.get_cursor()
1038 address = liststore.get_value(liststore.get_iter(path), 0)
1039 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1043 size = qr.getModuleCount()*boxsize
1044 def area_expose_cb(area, event):
1045 style = area.get_style()
1046 k = qr.getModuleCount()
1049 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1050 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1051 area = gtk.DrawingArea()
1052 area.set_size_request(size, size)
1053 area.connect("expose-event", area_expose_cb)
1055 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1056 dialog.vbox.add(area)
1060 button = gtk.Button("QR")
1061 button.connect("clicked", showqrcode, treeview, liststore)
1063 hbox.pack_start(button,False)
1065 button = gtk.Button("Copy to clipboard")
1066 def copy2clipboard(w, treeview, liststore):
1068 path, col = treeview.get_cursor()
1070 address = liststore.get_value( liststore.get_iter(path), 0)
1071 if platform.system() == 'Windows':
1072 from Tkinter import Tk
1076 r.clipboard_append( address )
1079 c = gtk.clipboard_get()
1080 c.set_text( address )
1081 button.connect("clicked", copy2clipboard, treeview, liststore)
1083 hbox.pack_start(button,False)
1086 button = gtk.Button("Pay to")
1087 def payto(w, treeview, liststore):
1088 path, col = treeview.get_cursor()
1090 address = liststore.get_value( liststore.get_iter(path), 0)
1091 self.payto_entry.set_text( address )
1092 self.notebook.set_current_page(1)
1093 self.amount_entry.grab_focus()
1095 button.connect("clicked", payto, treeview, liststore)
1097 hbox.pack_start(button,False)
1100 vbox.pack_start(scroll,True)
1101 vbox.pack_start(hbox, False)
1104 def update_status_bar(self):
1105 interface = self.wallet.interface
1106 if self.funds_error:
1107 text = "Not enough funds"
1108 elif interface and interface.is_connected:
1109 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1110 if not self.wallet.up_to_date:
1111 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1112 text = "Synchronizing..."
1114 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1115 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.verifier.height))
1116 c, u = self.wallet.get_balance()
1117 text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
1118 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
1120 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1121 self.network_button.set_tooltip_text("Not connected.")
1122 text = "Not connected"
1124 self.status_bar.pop(self.context_id)
1125 self.status_bar.push(self.context_id, text)
1127 if self.wallet.up_to_date and self.wallet_updated:
1128 self.update_history_tab()
1129 self.update_receiving_tab()
1130 # addressbook too...
1131 self.info.set_text( self.wallet.banner )
1132 self.wallet_updated = False
1134 def update_receiving_tab(self):
1135 self.recv_list.clear()
1136 for address in self.wallet.all_addresses():
1137 if self.wallet.is_change(address):continue
1138 label = self.wallet.labels.get(address)
1139 h = self.wallet.history.get(address,[])
1141 tx = "None" if n==0 else "%d"%n
1142 self.recv_list.append((address, label, tx ))
1144 def update_sending_tab(self):
1145 # detect addresses that are not mine in history, add them here...
1146 self.addressbook_list.clear()
1147 for alias, v in self.wallet.aliases.items():
1149 label = self.wallet.labels.get(alias)
1150 self.addressbook_list.append((alias, label, '-'))
1152 for address in self.wallet.addressbook:
1153 label = self.wallet.labels.get(address)
1155 for tx in self.wallet.transactions.values():
1156 if address in map(lambda x:x[0], tx.outputs): n += 1
1158 self.addressbook_list.append((address, label, "%d"%n))
1160 def update_history_tab(self):
1161 cursor = self.history_treeview.get_cursor()[0]
1162 self.history_list.clear()
1164 for item in self.wallet.get_tx_history():
1165 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1168 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1171 conf_icon = gtk.STOCK_APPLY
1173 time_str = 'pending'
1174 conf_icon = gtk.STOCK_EXECUTE
1176 label, is_default_label = self.wallet.get_label(tx_hash)
1177 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1178 details = self.wallet.get_tx_details(tx_hash)
1180 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1181 format_satoshis(value,True,self.wallet.num_zeros),
1182 format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] )
1183 if cursor: self.history_treeview.set_cursor( cursor )
1187 def newaddress_dialog(self, w):
1189 title = "New Contact"
1190 dialog = gtk.Dialog(title, parent=self.window,
1191 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1192 buttons= ("cancel", 0, "ok",1) )
1196 label_label = gtk.Label('Label:')
1197 label_label.set_size_request(120,10)
1199 label.pack_start(label_label)
1200 label_entry = gtk.Entry()
1202 label.pack_start(label_entry)
1204 dialog.vbox.pack_start(label, False, True, 5)
1206 address = gtk.HBox()
1207 address_label = gtk.Label('Address:')
1208 address_label.set_size_request(120,10)
1209 address_label.show()
1210 address.pack_start(address_label)
1211 address_entry = gtk.Entry()
1212 address_entry.show()
1213 address.pack_start(address_entry)
1215 dialog.vbox.pack_start(address, False, True, 5)
1217 result = dialog.run()
1218 address = address_entry.get_text()
1219 label = label_entry.get_text()
1223 if self.wallet.is_valid(address):
1224 self.wallet.addressbook.append(address)
1225 if label: self.wallet.labels[address] = label
1227 self.update_sending_tab()
1229 errorDialog = gtk.MessageDialog(
1231 flags=gtk.DIALOG_MODAL,
1232 buttons= gtk.BUTTONS_CLOSE,
1233 message_format = "Invalid address")
1236 errorDialog.destroy()
1240 class ElectrumGui():
1242 def __init__(self, wallet, config):
1243 self.wallet = wallet
1244 self.config = config
1246 def main(self, url=None):
1247 ew = ElectrumWindow(self.wallet, self.config)
1248 if url: ew.set_url(url)
1251 def restore_or_create(self):
1252 return restore_create_dialog(self.wallet)
1254 def seed_dialog(self):
1255 return run_recovery_dialog( self.wallet )
1257 def network_dialog(self):
1258 return run_network_dialog( self.wallet, parent=None )
1260 def show_seed(self):
1261 show_seed_dialog(self.wallet, None, None)
1263 def password_dialog(self):
1264 change_password_dialog(self.wallet, None, None)
1266 def restore_wallet(self):
1267 wallet = self.wallet
1269 dialog = gtk.MessageDialog(
1271 flags = gtk.DIALOG_MODAL,
1272 buttons = gtk.BUTTONS_CANCEL,
1273 message_format = "Please wait..." )
1276 def recover_thread( wallet, dialog ):
1277 while not wallet.is_up_to_date():
1279 gobject.idle_add( dialog.destroy )
1281 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1284 if r==gtk.RESPONSE_CANCEL: return False
1285 if not wallet.is_found():
1286 show_message("No transactions found for this seed")