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/>.
19 # use this because this file is named gtk.py
20 from __future__ import absolute_import
23 import thread, time, ast, sys, re
24 import socket, traceback
28 from decimal import Decimal
29 from electrum.util import print_error
30 from electrum.bitcoin import is_valid
31 from electrum import mnemonic, pyqrnative, WalletStorage, Wallet
33 gtk.gdk.threads_init()
36 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
38 from electrum.util import format_satoshis
39 from electrum.network import DEFAULT_SERVERS
40 from electrum.bitcoin import MIN_RELAY_TX_FEE
42 def numbify(entry, is_int = False):
43 text = entry.get_text().strip()
45 if not is_int: chars +='.'
46 s = ''.join([i for i in text if i in chars])
51 s = s[:p] + '.' + s[p:p+8]
53 amount = int( Decimal(s) * 100000000 )
67 def show_seed_dialog(wallet, password, parent):
69 show_message("No seed")
72 mnemonic = wallet.get_mnemonic(password)
74 show_message("Incorrect password")
76 dialog = gtk.MessageDialog(
78 flags = gtk.DIALOG_MODAL,
79 buttons = gtk.BUTTONS_OK,
80 message_format = "Your wallet generation seed is:\n\n" + '"' + mnemonic + '"'\
81 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" )
82 dialog.set_title("Seed")
87 def restore_create_dialog():
89 # ask if the user wants to create a new wallet, or recover from a seed.
90 # if he wants to recover, and nothing is found, do not create wallet
91 dialog = gtk.Dialog("electrum", parent=None,
92 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
93 buttons= ("create", 0, "restore",1, "cancel",2) )
95 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
97 dialog.vbox.pack_start(label)
102 if r==2: return False
103 return 'restore' if r==1 else 'create'
107 def run_recovery_dialog():
108 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
109 dialog = gtk.MessageDialog(
111 flags = gtk.DIALOG_MODAL,
112 buttons = gtk.BUTTONS_OK_CANCEL,
113 message_format = message)
116 dialog.set_default_response(gtk.RESPONSE_OK)
118 # ask seed, server and gap in the same dialog
119 seed_box = gtk.HBox()
120 seed_label = gtk.Label('Seed or mnemonic:')
121 seed_label.set_size_request(150,-1)
122 seed_box.pack_start(seed_label, False, False, 10)
124 seed_entry = gtk.Entry()
126 seed_entry.set_size_request(450,-1)
127 seed_box.pack_start(seed_entry, False, False, 10)
128 add_help_button(seed_box, '.')
130 vbox.pack_start(seed_box, False, False, 5)
134 seed = seed_entry.get_text()
137 if r==gtk.RESPONSE_CANCEL:
143 print_error("Warning: Not hex, trying decode")
144 seed = mnemonic.mn_decode( seed.split(' ') )
146 show_message("no seed")
153 def run_settings_dialog(self):
155 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
157 dialog = gtk.MessageDialog(
158 parent = self.window,
159 flags = gtk.DIALOG_MODAL,
160 buttons = gtk.BUTTONS_OK_CANCEL,
161 message_format = message)
164 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
166 dialog.set_image(image)
167 dialog.set_title("Settings")
170 dialog.set_default_response(gtk.RESPONSE_OK)
173 fee_entry = gtk.Entry()
174 fee_label = gtk.Label('Transaction fee:')
175 fee_label.set_size_request(150,10)
177 fee.pack_start(fee_label,False, False, 10)
178 fee_entry.set_text( str( Decimal(self.wallet.fee) /100000000 ) )
179 fee_entry.connect('changed', numbify, False)
181 fee.pack_start(fee_entry,False,False, 10)
182 add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001')
184 vbox.pack_start(fee, False,False, 5)
187 nz_entry = gtk.Entry()
188 nz_label = gtk.Label('Display zeros:')
189 nz_label.set_size_request(150,10)
191 nz.pack_start(nz_label,False, False, 10)
192 nz_entry.set_text( str( self.num_zeros ))
193 nz_entry.connect('changed', numbify, True)
195 nz.pack_start(nz_entry,False,False, 10)
196 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'")
198 vbox.pack_start(nz, False,False, 5)
202 fee = fee_entry.get_text()
203 nz = nz_entry.get_text()
206 if r==gtk.RESPONSE_CANCEL:
210 fee = int( 100000000 * Decimal(fee) )
212 show_message("error")
214 self.wallet.set_fee(fee)
220 show_message("error")
223 if self.num_zeros != nz:
225 self.config.set_key('num_zeros',nz,True)
226 self.update_history_tab()
231 def run_network_dialog( network, parent ):
233 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
235 if network.is_connected():
236 interface = network.interface
237 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, network.blockchain.height())
239 status = "Not connected"
242 status = "Please choose a server.\nSelect cancel if you are offline."
244 if network.is_connected():
245 server = interface.server
246 host, port, protocol = server.split(':')
248 servers = network.get_servers()
250 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
251 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
252 dialog.set_title("Server")
253 dialog.set_image(image)
257 host_box = gtk.HBox()
258 host_label = gtk.Label('Connect to:')
259 host_label.set_size_request(100,-1)
261 host_box.pack_start(host_label, False, False, 10)
262 host_entry = gtk.Entry()
263 host_entry.set_size_request(200,-1)
264 if network.is_connected():
265 host_entry.set_text(server)
267 host_entry.set_text("Not Connected")
269 host_box.pack_start(host_entry, False, False, 10)
270 add_help_button(host_box, 'The name, port number and protocol of your Electrum server, separated by a colon. Example: "ecdsa.org:50002:s". Some servers allow you to connect through http (port 80) or https (port 443)')
273 p_box = gtk.HBox(False, 10)
276 p_label = gtk.Label('Protocol:')
277 p_label.set_size_request(100,-1)
279 p_box.pack_start(p_label, False, False, 10)
281 combobox = gtk.combo_box_new_text()
283 combobox.append_text("TCP")
284 combobox.append_text("SSL")
285 combobox.append_text("HTTP")
286 combobox.append_text("HTTPS")
288 p_box.pack_start(combobox, True, True, 0)
291 return unicode(host_entry.get_text()).split(':')
293 def set_combobox(protocol):
294 combobox.set_active('tshg'.index(protocol))
296 def set_protocol(protocol):
297 host = current_line()[0]
299 if protocol not in pp.keys():
300 protocol = pp.keys()[0]
301 set_combobox(protocol)
303 host_entry.set_text( host + ':' + port + ':' + protocol)
305 combobox.connect("changed", lambda x:set_protocol('tshg'[combobox.get_active()]))
306 if network.is_connected():
307 set_combobox(protocol)
309 server_list = gtk.ListStore(str)
310 for host in servers.keys():
311 server_list.append([host])
313 treeview = gtk.TreeView(model=server_list)
316 label = 'Active Servers' if network.irc_servers else 'Default Servers'
317 tvcolumn = gtk.TreeViewColumn(label)
318 treeview.append_column(tvcolumn)
319 cell = gtk.CellRendererText()
320 tvcolumn.pack_start(cell, False)
321 tvcolumn.add_attribute(cell, 'text', 0)
323 vbox.pack_start(host_box, False,False, 5)
324 vbox.pack_start(p_box, True, True, 0)
326 #scroll = gtk.ScrolledWindow()
327 #scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
328 #scroll.add_with_viewport(treeview)
330 #vbox.pack_start(scroll, True)
331 vbox.pack_start(treeview, True)
333 def my_treeview_cb(treeview):
334 path, view_column = treeview.get_cursor()
335 host = server_list.get_value( server_list.get_iter(path), 0)
341 protocol = pp.keys()[0]
343 host_entry.set_text( host + ':' + port + ':' + protocol)
344 set_combobox(protocol)
346 treeview.connect('cursor-changed', my_treeview_cb)
350 server = host_entry.get_text()
353 if r==gtk.RESPONSE_CANCEL:
357 host, port, protocol = server.split(':')
358 proxy = network.config.get('proxy')
359 auto_connect = network.config.get('auto_cycle')
360 network.set_parameters(host, port, protocol, proxy, auto_connect)
362 show_message("error:" + server)
369 def show_message(message, parent=None):
370 dialog = gtk.MessageDialog(
372 flags = gtk.DIALOG_MODAL,
373 buttons = gtk.BUTTONS_CLOSE,
374 message_format = message )
379 def password_line(label):
380 password = gtk.HBox()
381 password_label = gtk.Label(label)
382 password_label.set_size_request(120,10)
383 password_label.show()
384 password.pack_start(password_label,False, False, 10)
385 password_entry = gtk.Entry()
386 password_entry.set_size_request(300,-1)
387 password_entry.set_visibility(False)
388 password_entry.show()
389 password.pack_start(password_entry,False,False, 10)
391 return password, password_entry
393 def password_dialog(parent):
394 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
395 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
396 dialog.get_image().set_visible(False)
397 current_pw, current_pw_entry = password_line('Password:')
398 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
399 dialog.vbox.pack_start(current_pw, False, True, 0)
401 result = dialog.run()
402 pw = current_pw_entry.get_text()
404 if result != gtk.RESPONSE_CANCEL: return pw
407 def change_password_dialog(is_encrypted, parent):
410 msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if is_encrypted else 'Your wallet keys are not encrypted'
412 msg = "Please choose a password to encrypt your wallet keys"
414 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
415 dialog.set_title("Change password")
417 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
419 dialog.set_image(image)
422 current_pw, current_pw_entry = password_line('Current password:')
423 dialog.vbox.pack_start(current_pw, False, True, 0)
425 password, password_entry = password_line('New password:')
426 dialog.vbox.pack_start(password, False, True, 5)
427 password2, password2_entry = password_line('Confirm password:')
428 dialog.vbox.pack_start(password2, False, True, 5)
431 result = dialog.run()
432 password = current_pw_entry.get_text() if is_encrypted else None
433 new_password = password_entry.get_text()
434 new_password2 = password2_entry.get_text()
436 if result == gtk.RESPONSE_CANCEL:
439 if new_password != new_password2:
440 show_message("passwords do not match")
441 return change_password_dialog(is_encrypted, parent)
446 return True, password, new_password
450 def add_help_button(hbox, message):
451 button = gtk.Button('?')
452 button.connect("clicked", lambda x: show_message(message))
454 hbox.pack_start(button,False, False)
457 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
459 gobject.type_register(MyWindow)
460 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
461 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
464 class ElectrumWindow:
466 def show_message(self, msg):
467 show_message(msg, self.window)
469 def __init__(self, wallet, config, network):
472 self.network = network
473 self.funds_error = False # True if not enough funds
474 self.num_zeros = int(self.config.get('num_zeros',0))
476 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
477 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
478 if not self.wallet.seed: title += ' [seedless]'
479 self.window.set_title(title)
480 self.window.connect("destroy", gtk.main_quit)
481 self.window.set_border_width(0)
482 self.window.connect('mykeypress', gtk.main_quit)
483 self.window.set_default_size(720, 350)
484 self.wallet_updated = False
488 self.notebook = gtk.Notebook()
489 self.create_history_tab()
491 self.create_send_tab()
492 self.create_recv_tab()
493 self.create_book_tab()
494 self.create_about_tab()
496 vbox.pack_start(self.notebook, True, True, 2)
498 self.status_bar = gtk.Statusbar()
499 vbox.pack_start(self.status_bar, False, False, 0)
501 self.status_image = gtk.Image()
502 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
503 self.status_image.set_alignment(True, 0.5 )
504 self.status_image.show()
506 self.network_button = gtk.Button()
507 self.network_button.connect("clicked", lambda x: run_network_dialog(self.network, self.window) )
508 self.network_button.add(self.status_image)
509 self.network_button.set_relief(gtk.RELIEF_NONE)
510 self.network_button.show()
511 self.status_bar.pack_end(self.network_button, False, False)
514 def seedb(w, wallet):
515 if wallet.use_encryption:
516 password = password_dialog(self.window)
517 if not password: return
518 else: password = None
519 show_seed_dialog(wallet, password, self.window)
520 button = gtk.Button('S')
521 button.connect("clicked", seedb, wallet )
522 button.set_relief(gtk.RELIEF_NONE)
524 self.status_bar.pack_end(button,False, False)
526 settings_icon = gtk.Image()
527 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
528 settings_icon.set_alignment(0.5, 0.5)
529 settings_icon.set_size_request(16,16 )
532 prefs_button = gtk.Button()
533 prefs_button.connect("clicked", lambda x: run_settings_dialog(self) )
534 prefs_button.add(settings_icon)
535 prefs_button.set_tooltip_text("Settings")
536 prefs_button.set_relief(gtk.RELIEF_NONE)
538 self.status_bar.pack_end(prefs_button,False,False)
540 self.pw_icon = gtk.Image()
541 self.pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
542 self.pw_icon.set_alignment(0.5, 0.5)
543 self.pw_icon.set_size_request(16,16 )
547 password_button = gtk.Button()
548 password_button.connect("clicked", self.do_update_password)
549 password_button.add(self.pw_icon)
550 password_button.set_relief(gtk.RELIEF_NONE)
551 password_button.show()
552 self.status_bar.pack_end(password_button,False,False)
554 self.window.add(vbox)
555 self.window.show_all()
558 self.context_id = self.status_bar.get_context_id("statusbar")
559 self.update_status_bar()
561 self.network.register_callback('updated', self.update_callback)
564 def update_status_bar_thread():
566 gobject.idle_add( self.update_status_bar )
570 def check_recipient_thread():
574 if self.payto_entry.is_focus():
576 r = self.payto_entry.get_text()
580 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
582 to_address = self.wallet.get_alias(r, interactive=False)
586 s = r + ' <' + to_address + '>'
587 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
590 thread.start_new_thread(update_status_bar_thread, ())
592 thread.start_new_thread(check_recipient_thread, ())
593 self.notebook.set_current_page(0)
595 def update_callback(self):
596 self.wallet_updated = True
598 def do_update_password(self):
600 show_message("No seed")
603 res = change_password_dialog(self.wallet.use_encryption, self.window)
605 _, password, new_password = res
608 wallet.get_seed(password)
610 show_message("Incorrect password")
613 wallet.update_password(password, new_password)
615 if wallet.use_encryption:
616 self.pw_icon.set_tooltip_text('wallet is encrypted')
618 self.pw_icon.set_tooltip_text('wallet is unencrypted')
621 def add_tab(self, page, name):
622 tab_label = gtk.Label(name)
624 self.notebook.append_page(page, tab_label)
627 def create_send_tab(self):
629 page = vbox = gtk.VBox()
633 payto_label = gtk.Label('Pay to:')
634 payto_label.set_size_request(100,-1)
635 payto.pack_start(payto_label, False)
636 payto_entry = gtk.Entry()
637 payto_entry.set_size_request(450, 26)
638 payto.pack_start(payto_entry, False)
639 vbox.pack_start(payto, False, False, 5)
642 message_label = gtk.Label('Description:')
643 message_label.set_size_request(100,-1)
644 message.pack_start(message_label, False)
645 message_entry = gtk.Entry()
646 message_entry.set_size_request(450, 26)
647 message.pack_start(message_entry, False)
648 vbox.pack_start(message, False, False, 5)
650 amount_box = gtk.HBox()
651 amount_label = gtk.Label('Amount:')
652 amount_label.set_size_request(100,-1)
653 amount_box.pack_start(amount_label, False)
654 amount_entry = gtk.Entry()
655 amount_entry.set_size_request(120, -1)
656 amount_box.pack_start(amount_entry, False)
657 vbox.pack_start(amount_box, False, False, 5)
659 self.fee_box = fee_box = gtk.HBox()
660 fee_label = gtk.Label('Fee:')
661 fee_label.set_size_request(100,-1)
662 fee_box.pack_start(fee_label, False)
663 fee_entry = gtk.Entry()
664 fee_entry.set_size_request(60, 26)
665 fee_box.pack_start(fee_entry, False)
666 vbox.pack_start(fee_box, False, False, 5)
669 empty_label = gtk.Label('')
670 empty_label.set_size_request(100,-1)
671 end_box.pack_start(empty_label, False)
672 send_button = gtk.Button("Send")
674 end_box.pack_start(send_button, False, False, 0)
675 clear_button = gtk.Button("Clear")
677 end_box.pack_start(clear_button, False, False, 15)
678 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
679 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
681 vbox.pack_start(end_box, False, False, 5)
683 # display this line only if there is a signature
684 payto_sig = gtk.HBox()
685 payto_sig_id = gtk.Label('')
686 payto_sig.pack_start(payto_sig_id, False)
687 vbox.pack_start(payto_sig, True, True, 5)
690 self.user_fee = False
692 def entry_changed( entry, is_fee ):
693 self.funds_error = False
694 amount = numbify(amount_entry)
695 fee = numbify(fee_entry)
696 if not is_fee: fee = None
699 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
701 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
704 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
705 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
706 send_button.set_sensitive(True)
708 send_button.set_sensitive(False)
709 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
710 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
711 self.funds_error = True
713 amount_entry.connect('changed', entry_changed, False)
714 fee_entry.connect('changed', entry_changed, True)
716 self.payto_entry = payto_entry
717 self.payto_fee_entry = fee_entry
718 self.payto_sig_id = payto_sig_id
719 self.payto_sig = payto_sig
720 self.amount_entry = amount_entry
721 self.message_entry = message_entry
722 self.add_tab(page, 'Send')
724 def set_frozen(self,entry,frozen):
726 entry.set_editable(False)
727 entry.set_has_frame(False)
728 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
730 entry.set_editable(True)
731 entry.set_has_frame(True)
732 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
734 def set_url(self, url):
735 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
736 self.notebook.set_current_page(1)
737 self.payto_entry.set_text(payto)
738 self.message_entry.set_text(message)
739 self.amount_entry.set_text(amount)
741 self.set_frozen(self.payto_entry,True)
742 self.set_frozen(self.amount_entry,True)
743 self.set_frozen(self.message_entry,True)
744 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
746 self.payto_sig.set_visible(False)
748 def create_about_tab(self):
753 tv.set_editable(False)
754 tv.set_cursor_visible(False)
755 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
756 scroll = gtk.ScrolledWindow()
758 page.pack_start(scroll)
759 self.info = tv.get_buffer()
760 self.add_tab(page, 'Wall')
762 def do_clear(self, w, data):
763 self.payto_sig.set_visible(False)
764 self.payto_fee_entry.set_text('')
765 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
766 self.set_frozen(entry,False)
769 def question(self,msg):
770 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
772 result = dialog.run()
774 return result == gtk.RESPONSE_OK
776 def do_send(self, w, data):
777 payto_entry, label_entry, amount_entry, fee_entry = data
778 label = label_entry.get_text()
779 r = payto_entry.get_text()
782 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
783 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
786 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
790 self.update_sending_tab()
793 to_address = m2.group(5)
797 if not is_valid(to_address):
798 self.show_message( "invalid bitcoin address:\n"+to_address)
802 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
804 self.show_message( "invalid amount")
807 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
809 self.show_message( "invalid fee")
812 if self.wallet.use_encryption:
813 password = password_dialog(self.window)
820 tx = self.wallet.mktx( [(to_address, amount)], password, fee )
821 except Exception as e:
822 self.show_message(str(e))
825 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
826 self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." )
831 self.wallet.labels[tx.hash()] = label
833 status, msg = self.wallet.sendtx( tx )
835 self.show_message( "payment sent.\n" + msg )
836 payto_entry.set_text("")
837 label_entry.set_text("")
838 amount_entry.set_text("")
839 fee_entry.set_text("")
841 self.update_sending_tab()
843 self.show_message( msg )
846 def treeview_button_press(self, treeview, event):
847 if event.type == gtk.gdk._2BUTTON_PRESS:
848 c = treeview.get_cursor()[0]
849 if treeview == self.history_treeview:
850 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
851 self.show_message(tx_details)
852 elif treeview == self.contacts_treeview:
853 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
854 #a = self.wallet.aliases.get(m)
856 # if a[0] in self.wallet.authorities.keys():
857 # s = self.wallet.authorities.get(a[0])
860 # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
861 # self.show_message(msg)
864 def treeview_key_press(self, treeview, event):
865 c = treeview.get_cursor()[0]
866 if event.keyval == gtk.keysyms.Up:
868 treeview.parent.grab_focus()
869 treeview.set_cursor((0,))
870 elif event.keyval == gtk.keysyms.Return:
871 if treeview == self.history_treeview:
872 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
873 self.show_message(tx_details)
874 elif treeview == self.contacts_treeview:
875 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
876 #a = self.wallet.aliases.get(m)
878 # if a[0] in self.wallet.authorities.keys():
879 # s = self.wallet.authorities.get(a[0])
882 # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
883 # self.show_message(msg)
887 def create_history_tab(self):
889 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
890 treeview = gtk.TreeView(model=self.history_list)
891 self.history_treeview = treeview
892 treeview.set_tooltip_column(7)
894 treeview.connect('key-press-event', self.treeview_key_press)
895 treeview.connect('button-press-event', self.treeview_button_press)
897 tvcolumn = gtk.TreeViewColumn('')
898 treeview.append_column(tvcolumn)
899 cell = gtk.CellRendererPixbuf()
900 tvcolumn.pack_start(cell, False)
901 tvcolumn.set_attributes(cell, stock_id=1)
903 tvcolumn = gtk.TreeViewColumn('Date')
904 treeview.append_column(tvcolumn)
905 cell = gtk.CellRendererText()
906 tvcolumn.pack_start(cell, False)
907 tvcolumn.add_attribute(cell, 'text', 2)
909 tvcolumn = gtk.TreeViewColumn('Description')
910 treeview.append_column(tvcolumn)
911 cell = gtk.CellRendererText()
912 cell.set_property('foreground', 'grey')
913 cell.set_property('family', MONOSPACE_FONT)
914 cell.set_property('editable', True)
915 def edited_cb(cell, path, new_text, h_list):
916 tx = h_list.get_value( h_list.get_iter(path), 0)
917 self.wallet.set_label(tx,new_text)
918 self.update_history_tab()
919 cell.connect('edited', edited_cb, self.history_list)
920 def editing_started(cell, entry, path, h_list):
921 tx = h_list.get_value( h_list.get_iter(path), 0)
922 if not self.wallet.labels.get(tx): entry.set_text('')
923 cell.connect('editing-started', editing_started, self.history_list)
924 tvcolumn.set_expand(True)
925 tvcolumn.pack_start(cell, True)
926 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
928 tvcolumn = gtk.TreeViewColumn('Amount')
929 treeview.append_column(tvcolumn)
930 cell = gtk.CellRendererText()
931 cell.set_alignment(1, 0.5)
932 cell.set_property('family', MONOSPACE_FONT)
933 tvcolumn.pack_start(cell, False)
934 tvcolumn.add_attribute(cell, 'text', 5)
936 tvcolumn = gtk.TreeViewColumn('Balance')
937 treeview.append_column(tvcolumn)
938 cell = gtk.CellRendererText()
939 cell.set_alignment(1, 0.5)
940 cell.set_property('family', MONOSPACE_FONT)
941 tvcolumn.pack_start(cell, False)
942 tvcolumn.add_attribute(cell, 'text', 6)
944 tvcolumn = gtk.TreeViewColumn('Tooltip')
945 treeview.append_column(tvcolumn)
946 cell = gtk.CellRendererText()
947 tvcolumn.pack_start(cell, False)
948 tvcolumn.add_attribute(cell, 'text', 7)
949 tvcolumn.set_visible(False)
951 scroll = gtk.ScrolledWindow()
952 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
955 self.add_tab(scroll, 'History')
956 self.update_history_tab()
959 def create_recv_tab(self):
960 self.recv_list = gtk.ListStore(str, str, str, str, str)
961 self.add_tab( self.make_address_list(True), 'Receive')
962 self.update_receiving_tab()
964 def create_book_tab(self):
965 self.addressbook_list = gtk.ListStore(str, str, str)
966 self.add_tab( self.make_address_list(False), 'Contacts')
967 self.update_sending_tab()
969 def make_address_list(self, is_recv):
970 liststore = self.recv_list if is_recv else self.addressbook_list
971 treeview = gtk.TreeView(model= liststore)
972 treeview.connect('key-press-event', self.treeview_key_press)
973 treeview.connect('button-press-event', self.treeview_button_press)
976 self.contacts_treeview = treeview
978 tvcolumn = gtk.TreeViewColumn('Address')
979 treeview.append_column(tvcolumn)
980 cell = gtk.CellRendererText()
981 cell.set_property('family', MONOSPACE_FONT)
982 tvcolumn.pack_start(cell, True)
983 tvcolumn.add_attribute(cell, 'text', 0)
985 tvcolumn = gtk.TreeViewColumn('Label')
986 tvcolumn.set_expand(True)
987 treeview.append_column(tvcolumn)
988 cell = gtk.CellRendererText()
989 cell.set_property('editable', True)
990 def edited_cb2(cell, path, new_text, liststore):
991 address = liststore.get_value( liststore.get_iter(path), 0)
992 self.wallet.set_label(address, new_text)
993 self.update_receiving_tab()
994 self.update_sending_tab()
995 self.update_history_tab()
996 cell.connect('edited', edited_cb2, liststore)
997 tvcolumn.pack_start(cell, True)
998 tvcolumn.add_attribute(cell, 'text', 1)
1000 tvcolumn = gtk.TreeViewColumn('Tx')
1001 treeview.append_column(tvcolumn)
1002 cell = gtk.CellRendererText()
1003 tvcolumn.pack_start(cell, True)
1004 tvcolumn.add_attribute(cell, 'text', 2)
1007 tvcolumn = gtk.TreeViewColumn('Balance')
1008 treeview.append_column(tvcolumn)
1009 cell = gtk.CellRendererText()
1010 tvcolumn.pack_start(cell, True)
1011 tvcolumn.add_attribute(cell, 'text', 3)
1012 tvcolumn = gtk.TreeViewColumn('Type')
1013 treeview.append_column(tvcolumn)
1014 cell = gtk.CellRendererText()
1015 tvcolumn.pack_start(cell, True)
1016 tvcolumn.add_attribute(cell, 'text', 4)
1018 scroll = gtk.ScrolledWindow()
1019 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1020 scroll.add(treeview)
1024 button = gtk.Button("New")
1025 button.connect("clicked", self.newaddress_dialog)
1027 hbox.pack_start(button,False)
1029 def showqrcode(w, treeview, liststore):
1030 path, col = treeview.get_cursor()
1032 address = liststore.get_value(liststore.get_iter(path), 0)
1033 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1037 size = qr.getModuleCount()*boxsize
1038 def area_expose_cb(area, event):
1039 style = area.get_style()
1040 k = qr.getModuleCount()
1043 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1044 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1045 area = gtk.DrawingArea()
1046 area.set_size_request(size, size)
1047 area.connect("expose-event", area_expose_cb)
1049 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1050 dialog.vbox.add(area)
1054 button = gtk.Button("QR")
1055 button.connect("clicked", showqrcode, treeview, liststore)
1057 hbox.pack_start(button,False)
1059 button = gtk.Button("Copy to clipboard")
1060 def copy2clipboard(w, treeview, liststore):
1062 path, col = treeview.get_cursor()
1064 address = liststore.get_value( liststore.get_iter(path), 0)
1065 if platform.system() == 'Windows':
1066 from Tkinter import Tk
1070 r.clipboard_append( address )
1073 c = gtk.clipboard_get()
1074 c.set_text( address )
1075 button.connect("clicked", copy2clipboard, treeview, liststore)
1077 hbox.pack_start(button,False)
1080 button = gtk.Button("Freeze")
1081 def freeze_address(w, treeview, liststore, wallet):
1082 path, col = treeview.get_cursor()
1084 address = liststore.get_value( liststore.get_iter(path), 0)
1085 if address in wallet.frozen_addresses:
1086 wallet.unfreeze(address)
1088 wallet.freeze(address)
1089 self.update_receiving_tab()
1090 button.connect("clicked", freeze_address, treeview, liststore, self.wallet)
1092 hbox.pack_start(button,False)
1095 button = gtk.Button("Pay to")
1096 def payto(w, treeview, liststore):
1097 path, col = treeview.get_cursor()
1099 address = liststore.get_value( liststore.get_iter(path), 0)
1100 self.payto_entry.set_text( address )
1101 self.notebook.set_current_page(1)
1102 self.amount_entry.grab_focus()
1104 button.connect("clicked", payto, treeview, liststore)
1106 hbox.pack_start(button,False)
1109 vbox.pack_start(scroll,True)
1110 vbox.pack_start(hbox, False)
1113 def update_status_bar(self):
1114 interface = self.network.interface
1115 if self.funds_error:
1116 text = "Not enough funds"
1117 elif interface and interface.is_connected:
1118 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height()))
1119 if not self.wallet.up_to_date:
1120 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1121 text = "Synchronizing..."
1123 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1124 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height()))
1125 c, u = self.wallet.get_balance()
1126 text = "Balance: %s "%( format_satoshis(c,False,self.num_zeros) )
1127 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.num_zeros).strip() )
1129 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1130 self.network_button.set_tooltip_text("Not connected.")
1131 text = "Not connected"
1133 self.status_bar.pop(self.context_id)
1134 self.status_bar.push(self.context_id, text)
1136 if self.wallet.up_to_date and self.wallet_updated:
1137 self.update_history_tab()
1138 self.update_receiving_tab()
1139 # addressbook too...
1140 self.info.set_text( self.network.banner )
1141 self.wallet_updated = False
1143 def update_receiving_tab(self):
1144 self.recv_list.clear()
1145 for address in self.wallet.addresses(True):
1148 if self.wallet.is_change(address): Type = "C"
1149 if address in self.wallet.imported_keys.keys():
1151 c, u = self.wallet.get_addr_balance(address)
1152 if address in self.wallet.frozen_addresses: Type = Type + "F"
1153 label = self.wallet.labels.get(address)
1154 h = self.wallet.history.get(address,[])
1156 tx = "0" if n==0 else "%d"%n
1157 self.recv_list.append((address, label, tx, format_satoshis(c,False,self.num_zeros), Type ))
1159 def update_sending_tab(self):
1160 # detect addresses that are not mine in history, add them here...
1161 self.addressbook_list.clear()
1162 #for alias, v in self.wallet.aliases.items():
1164 # label = self.wallet.labels.get(alias)
1165 # self.addressbook_list.append((alias, label, '-'))
1167 for address in self.wallet.addressbook:
1168 label = self.wallet.labels.get(address)
1169 n = self.wallet.get_num_tx(address)
1170 self.addressbook_list.append((address, label, "%d"%n))
1172 def update_history_tab(self):
1173 cursor = self.history_treeview.get_cursor()[0]
1174 self.history_list.clear()
1176 for item in self.wallet.get_tx_history():
1177 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1180 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1183 conf_icon = gtk.STOCK_APPLY
1185 time_str = 'unverified'
1188 time_str = 'pending'
1189 conf_icon = gtk.STOCK_EXECUTE
1191 label, is_default_label = self.wallet.get_label(tx_hash)
1192 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1193 details = self.get_tx_details(tx_hash)
1195 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1196 format_satoshis(value,True,self.num_zeros, whitespaces=True),
1197 format_satoshis(balance,False,self.num_zeros, whitespaces=True), tooltip, details] )
1198 if cursor: self.history_treeview.set_cursor( cursor )
1201 def get_tx_details(self, tx_hash):
1203 if not tx_hash: return ''
1204 tx = self.wallet.transactions.get(tx_hash)
1205 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
1206 conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1209 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1211 time_str = 'pending'
1213 inputs = map(lambda x: x.get('address'), tx.inputs)
1214 outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1215 tx_details = "Transaction Details" +"\n\n" \
1216 + "Transaction ID:\n" + tx_hash + "\n\n" \
1217 + "Status: %d confirmations\n"%conf
1220 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1221 + "Transaction fee: %s\n"% format_satoshis(fee, False)
1223 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1224 + "Transaction fee: unknown\n"
1226 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1228 tx_details += "Date: %s\n\n"%time_str \
1229 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1230 + "Outputs:\n-"+ '\n-'.join(outputs)
1236 def newaddress_dialog(self, w):
1238 title = "New Contact"
1239 dialog = gtk.Dialog(title, parent=self.window,
1240 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1241 buttons= ("cancel", 0, "ok",1) )
1245 label_label = gtk.Label('Label:')
1246 label_label.set_size_request(120,10)
1248 label.pack_start(label_label)
1249 label_entry = gtk.Entry()
1251 label.pack_start(label_entry)
1253 dialog.vbox.pack_start(label, False, True, 5)
1255 address = gtk.HBox()
1256 address_label = gtk.Label('Address:')
1257 address_label.set_size_request(120,10)
1258 address_label.show()
1259 address.pack_start(address_label)
1260 address_entry = gtk.Entry()
1261 address_entry.show()
1262 address.pack_start(address_entry)
1264 dialog.vbox.pack_start(address, False, True, 5)
1266 result = dialog.run()
1267 address = address_entry.get_text()
1268 label = label_entry.get_text()
1272 if is_valid(address):
1273 self.wallet.add_contact(address,label)
1274 self.update_sending_tab()
1276 errorDialog = gtk.MessageDialog(
1278 flags=gtk.DIALOG_MODAL,
1279 buttons= gtk.BUTTONS_CLOSE,
1280 message_format = "Invalid address")
1283 errorDialog.destroy()
1287 class ElectrumGui():
1289 def __init__(self, config, network):
1290 self.network = network
1291 self.config = config
1294 def main(self, url=None):
1296 storage = WalletStorage(self.config)
1297 if not storage.file_exists:
1298 action = self.restore_or_create()
1301 self.wallet = wallet = Wallet(storage)
1302 gap = self.config.get('gap_limit', 5)
1304 wallet.gap_limit = gap
1305 wallet.storage.put('gap_limit', gap, True)
1308 if action == 'create':
1309 wallet.init_seed(None)
1310 show_seed_dialog(wallet, None, None)
1311 r = change_password_dialog(False, None)
1312 password = r[2] if r else None
1313 print "password", password
1314 wallet.save_seed(password)
1315 wallet.synchronize() # generate first addresses offline
1317 elif action == 'restore':
1318 seed = self.seed_dialog()
1319 wallet.init_seed(seed)
1320 r = change_password_dialog(False, None)
1321 password = r[2] if r else None
1322 wallet.save_seed(password)
1327 self.wallet = Wallet(storage)
1330 self.wallet.start_threads(self.network)
1332 if action == 'restore':
1333 self.restore_wallet(wallet)
1335 w = ElectrumWindow(self.wallet, self.config, self.network)
1336 if url: w.set_url(url)
1339 def restore_or_create(self):
1340 return restore_create_dialog()
1342 def seed_dialog(self):
1343 return run_recovery_dialog()
1345 def network_dialog(self):
1346 return run_network_dialog( self.network, parent=None )
1349 def restore_wallet(self, wallet):
1351 dialog = gtk.MessageDialog(
1353 flags = gtk.DIALOG_MODAL,
1354 buttons = gtk.BUTTONS_CANCEL,
1355 message_format = "Please wait..." )
1358 def recover_thread( wallet, dialog ):
1359 wallet.restore(lambda x:x)
1360 gobject.idle_add( dialog.destroy )
1362 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1365 if r==gtk.RESPONSE_CANCEL: return False
1366 if not wallet.is_found():
1367 show_message("No transactions found for this seed")