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/>.
21 import thread, time, ast, sys, re
22 import socket, traceback
24 gi.require_version('Gtk', '3.0')
25 from gi.repository import Gtk, Gdk, GObject, cairo
26 from decimal import Decimal
27 from electrum.util import print_error
28 from electrum.bitcoin import is_valid
29 from electrum import mnemonic, pyqrnative, WalletStorage, Wallet
34 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
36 from electrum.util import format_satoshis
37 from electrum.network import DEFAULT_SERVERS
38 from electrum.bitcoin import MIN_RELAY_TX_FEE
40 def numbify(entry, is_int = False):
41 text = entry.get_text().strip()
43 if not is_int: chars +='.'
44 s = ''.join([i for i in text if i in chars])
49 s = s[:p] + '.' + s[p:p+8]
51 amount = int( Decimal(s) * 100000000 )
65 def show_seed_dialog(wallet, password, parent):
67 show_message("No seed")
70 mnemonic = wallet.get_mnemonic(password)
72 show_message("Incorrect password")
74 dialog = Gtk.MessageDialog(
76 flags = Gtk.DialogFlags.MODAL,
77 buttons = Gtk.ButtonsType.OK,
78 message_format = "Your wallet generation seed is:\n\n" + '"' + mnemonic + '"'\
79 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" )
80 dialog.set_title("Seed")
85 def restore_create_dialog():
87 # ask if the user wants to create a new wallet, or recover from a seed.
88 # if he wants to recover, and nothing is found, do not create wallet
89 dialog = Gtk.Dialog("electrum", parent=None,
90 flags=Gtk.DialogFlags.MODAL|Gtk.DialogFlags.NO_SEPARATOR,
91 buttons= ("create", 0, "restore",1, "cancel",2) )
93 label = Gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
95 dialog.vbox.pack_start(label, True, True, 0)
100 if r==2: return False
101 return 'restore' if r==1 else 'create'
105 def run_recovery_dialog():
106 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
107 dialog = Gtk.MessageDialog(
109 flags = Gtk.DialogFlags.MODAL,
110 buttons = Gtk.ButtonsType.OK_CANCEL,
111 message_format = message)
114 dialog.set_default_response(Gtk.ResponseType.OK)
116 # ask seed, server and gap in the same dialog
117 seed_box = Gtk.HBox()
118 seed_label = Gtk.Label(label='Seed or mnemonic:')
119 seed_label.set_size_request(150,-1)
120 seed_box.pack_start(seed_label, False, False, 10)
122 seed_entry = Gtk.Entry()
124 seed_entry.set_size_request(450,-1)
125 seed_box.pack_start(seed_entry, False, False, 10)
126 add_help_button(seed_box, '.')
128 vbox.pack_start(seed_box, False, False, 5)
132 seed = seed_entry.get_text()
135 if r==Gtk.ResponseType.CANCEL:
141 print_error("Warning: Not hex, trying decode")
142 seed = mnemonic.mn_decode( seed.split(' ') )
144 show_message("no seed")
151 def run_settings_dialog(self):
153 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
155 dialog = Gtk.MessageDialog(
156 parent = self.window,
157 flags = Gtk.DialogFlags.MODAL,
158 buttons = Gtk.ButtonsType.OK_CANCEL,
159 message_format = message)
162 image.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.DIALOG)
164 dialog.set_image(image)
165 dialog.set_title("Settings")
168 dialog.set_default_response(Gtk.ResponseType.OK)
171 fee_entry = Gtk.Entry()
172 fee_label = Gtk.Label(label='Transaction fee:')
173 fee_label.set_size_request(150,10)
175 fee.pack_start(fee_label,False, False, 10)
176 fee_entry.set_text( str( Decimal(self.wallet.fee) /100000000 ) )
177 fee_entry.connect('changed', numbify, False)
179 fee.pack_start(fee_entry,False,False, 10)
180 add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001')
182 vbox.pack_start(fee, False,False, 5)
185 nz_entry = Gtk.Entry()
186 nz_label = Gtk.Label(label='Display zeros:')
187 nz_label.set_size_request(150,10)
189 nz.pack_start(nz_label,False, False, 10)
190 nz_entry.set_text( str( self.num_zeros ))
191 nz_entry.connect('changed', numbify, True)
193 nz.pack_start(nz_entry,False,False, 10)
194 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'")
196 vbox.pack_start(nz, False,False, 5)
200 fee = fee_entry.get_text()
201 nz = nz_entry.get_text()
204 if r==Gtk.ResponseType.CANCEL:
208 fee = int( 100000000 * Decimal(fee) )
210 show_message("error")
212 self.wallet.set_fee(fee)
218 show_message("error")
221 if self.num_zeros != nz:
223 self.config.set_key('num_zeros',nz,True)
224 self.update_history_tab()
229 def run_network_dialog( network, parent ):
231 image.set_from_stock(Gtk.STOCK_NETWORK, Gtk.IconSize.DIALOG)
233 if network.is_connected():
234 interface = network.interface
235 status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, network.blockchain.height())
237 status = "Not connected"
240 status = "Please choose a server.\nSelect cancel if you are offline."
242 if network.is_connected():
243 server = interface.server
244 host, port, protocol = server.split(':')
246 servers = network.get_servers()
248 dialog = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
249 Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, status)
250 dialog.set_title("Server")
251 dialog.set_image(image)
255 host_box = Gtk.HBox()
256 host_label = Gtk.Label(label='Connect to:')
257 host_label.set_size_request(100,-1)
259 host_box.pack_start(host_label, False, False, 10)
260 host_entry = Gtk.Entry()
261 host_entry.set_size_request(200,-1)
262 if network.is_connected():
263 host_entry.set_text(server)
265 host_entry.set_text("Not Connected")
267 host_box.pack_start(host_entry, False, False, 10)
268 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)')
271 p_box = Gtk.HBox(False, 10)
274 p_label = Gtk.Label(label='Protocol:')
275 p_label.set_size_request(100,-1)
277 p_box.pack_start(p_label, False, False, 10)
279 combobox = Gtk.ComboBoxText()
281 combobox.append_text("TCP")
282 combobox.append_text("SSL")
283 combobox.append_text("HTTP")
284 combobox.append_text("HTTPS")
286 p_box.pack_start(combobox, True, True, 0)
289 return unicode(host_entry.get_text()).split(':')
291 def set_combobox(protocol):
292 combobox.set_active('tshg'.index(protocol))
294 def set_protocol(protocol):
295 host = current_line()[0]
297 if protocol not in pp.keys():
298 protocol = pp.keys()[0]
299 set_combobox(protocol)
301 host_entry.set_text( host + ':' + port + ':' + protocol)
303 combobox.connect("changed", lambda x:set_protocol('tshg'[combobox.get_active()]))
304 if network.is_connected():
305 set_combobox(protocol)
307 server_list = Gtk.ListStore(str)
308 for host in servers.keys():
309 server_list.append([host])
311 treeview = Gtk.TreeView(model=server_list)
314 label = 'Active Servers' if network.irc_servers else 'Default Servers'
315 tvcolumn = Gtk.TreeViewColumn(label)
316 treeview.append_column(tvcolumn)
317 cell = Gtk.CellRendererText()
318 tvcolumn.pack_start(cell, False)
319 tvcolumn.add_attribute(cell, 'text', 0)
321 vbox.pack_start(host_box, False,False, 5)
322 vbox.pack_start(p_box, True, True, 0)
324 #scroll = Gtk.ScrolledWindow()
325 #scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
326 #scroll.add_with_viewport(treeview)
328 #vbox.pack_start(scroll, True)
329 vbox.pack_start(treeview, True, True, 0)
331 def my_treeview_cb(treeview):
332 path, view_column = treeview.get_cursor()
333 host = server_list.get_value( server_list.get_iter(path), 0)
339 protocol = pp.keys()[0]
341 host_entry.set_text( host + ':' + port + ':' + protocol)
342 set_combobox(protocol)
344 treeview.connect('cursor-changed', my_treeview_cb)
348 server = host_entry.get_text()
351 if r==Gtk.ResponseType.CANCEL:
355 host, port, protocol = server.split(':')
356 proxy = network.config.get('proxy')
357 auto_connect = network.config.get('auto_cycle')
358 network.set_parameters(host, port, protocol, proxy, auto_connect)
360 show_message("error:" + server)
367 def show_message(message, parent=None):
368 dialog = Gtk.MessageDialog(
370 flags = Gtk.DialogFlags.MODAL,
371 buttons = Gtk.ButtonsType.CLOSE,
372 message_format = message )
377 def password_line(label):
378 password = Gtk.HBox()
379 password_label = Gtk.Label(label=label)
380 password_label.set_size_request(120,10)
381 password_label.show()
382 password.pack_start(password_label,False, False, 10)
383 password_entry = Gtk.Entry()
384 password_entry.set_size_request(300,-1)
385 password_entry.set_visibility(False)
386 password_entry.show()
387 password.pack_start(password_entry,False,False, 10)
389 return password, password_entry
391 def password_dialog(parent):
392 dialog = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
393 Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, "Please enter your password.")
394 dialog.get_image().set_visible(False)
395 current_pw, current_pw_entry = password_line('Password:')
396 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, Gtk.ResponseType.OK)
397 dialog.vbox.pack_start(current_pw, False, True, 0)
399 result = dialog.run()
400 pw = current_pw_entry.get_text()
402 if result != Gtk.ResponseType.CANCEL: return pw
405 def change_password_dialog(is_encrypted, parent):
408 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'
410 msg = "Please choose a password to encrypt your wallet keys"
412 dialog = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, msg)
413 dialog.set_title("Change password")
415 image.set_from_stock(Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.DIALOG)
417 dialog.set_image(image)
420 current_pw, current_pw_entry = password_line('Current password:')
421 dialog.vbox.pack_start(current_pw, False, True, 0)
423 password, password_entry = password_line('New password:')
424 dialog.vbox.pack_start(password, False, True, 5)
425 password2, password2_entry = password_line('Confirm password:')
426 dialog.vbox.pack_start(password2, False, True, 5)
429 result = dialog.run()
430 password = current_pw_entry.get_text() if is_encrypted else None
431 new_password = password_entry.get_text()
432 new_password2 = password2_entry.get_text()
434 if result == Gtk.ResponseType.CANCEL:
437 if new_password != new_password2:
438 show_message("passwords do not match")
439 return change_password_dialog(is_encrypted, parent)
444 return True, password, new_password
448 def add_help_button(hbox, message):
449 button = Gtk.Button('?')
450 button.connect("clicked", lambda x: show_message(message))
452 hbox.pack_start(button,False, False, 0)
455 class MyWindow(Gtk.Window): __gsignals__ = dict( mykeypress = (GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, None, (str,)) )
457 GObject.type_register(MyWindow)
458 #FIXME: can't find docs how to create keybindings in PyGI
459 #Gtk.binding_entry_add_signall(MyWindow, Gdk.KEY_W, Gdk.ModifierType.CONTROL_MASK, 'mykeypress', ['ctrl+W'])
460 #Gtk.binding_entry_add_signall(MyWindow, Gdk.KEY_Q, Gdk.ModifierType.CONTROL_MASK, 'mykeypress', ['ctrl+Q'])
463 class ElectrumWindow:
465 def show_message(self, msg):
466 show_message(msg, self.window)
468 def __init__(self, wallet, config, network):
471 self.network = network
472 self.funds_error = False # True if not enough funds
473 self.num_zeros = int(self.config.get('num_zeros',0))
475 self.window = MyWindow(Gtk.WindowType.TOPLEVEL)
476 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
477 if not self.wallet.seed: title += ' [seedless]'
478 self.window.set_title(title)
479 self.window.connect("destroy", Gtk.main_quit)
480 self.window.set_border_width(0)
481 self.window.connect('mykeypress', Gtk.main_quit)
482 self.window.set_default_size(720, 350)
483 self.wallet_updated = False
487 self.notebook = Gtk.Notebook()
488 self.create_history_tab()
490 self.create_send_tab()
491 self.create_recv_tab()
492 self.create_book_tab()
493 self.create_about_tab()
495 vbox.pack_start(self.notebook, True, True, 2)
497 self.status_bar = Gtk.Statusbar()
498 vbox.pack_start(self.status_bar, False, False, 0)
500 self.status_image = Gtk.Image()
501 self.status_image.set_from_stock(Gtk.STOCK_NO, Gtk.IconSize.MENU)
502 self.status_image.set_alignment(True, 0.5 )
503 self.status_image.show()
505 self.network_button = Gtk.Button()
506 self.network_button.connect("clicked", lambda x: run_network_dialog(self.network, self.window) )
507 self.network_button.add(self.status_image)
508 self.network_button.set_relief(Gtk.ReliefStyle.NONE)
509 self.network_button.show()
510 self.status_bar.pack_end(self.network_button, False, False, 0)
513 def seedb(w, wallet):
514 if wallet.use_encryption:
515 password = password_dialog(self.window)
516 if not password: return
517 else: password = None
518 show_seed_dialog(wallet, password, self.window)
519 button = Gtk.Button('S')
520 button.connect("clicked", seedb, self.wallet )
521 button.set_relief(Gtk.ReliefStyle.NONE)
523 self.status_bar.pack_end(button,False, False, 0)
525 settings_icon = Gtk.Image()
526 settings_icon.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU)
527 settings_icon.set_alignment(0.5, 0.5)
528 settings_icon.set_size_request(16,16 )
531 prefs_button = Gtk.Button()
532 prefs_button.connect("clicked", lambda x: run_settings_dialog(self) )
533 prefs_button.add(settings_icon)
534 prefs_button.set_tooltip_text("Settings")
535 prefs_button.set_relief(Gtk.ReliefStyle.NONE)
537 self.status_bar.pack_end(prefs_button,False,False, 0)
539 self.pw_icon = Gtk.Image()
540 self.pw_icon.set_from_stock(Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.MENU)
541 self.pw_icon.set_alignment(0.5, 0.5)
542 self.pw_icon.set_size_request(16,16 )
547 if self.wallet.use_encryption:
548 self.pw_icon.set_tooltip_text('Wallet is encrypted')
550 self.pw_icon.set_tooltip_text('Wallet is unencrypted')
552 password_button = Gtk.Button()
553 password_button.connect("clicked", self.do_update_password, self.wallet)
554 password_button.add(self.pw_icon)
555 password_button.set_relief(Gtk.ReliefStyle.NONE)
556 password_button.show()
557 self.status_bar.pack_end(password_button,False,False, 0)
559 self.window.add(vbox)
560 self.window.show_all()
563 self.context_id = self.status_bar.get_context_id("statusbar")
564 self.update_status_bar()
566 self.network.register_callback('updated', self.update_callback)
569 def update_status_bar_thread():
571 GObject.idle_add( self.update_status_bar )
575 def check_recipient_thread():
579 if self.payto_entry.is_focus():
581 r = self.payto_entry.get_text()
585 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
587 to_address = self.wallet.get_alias(r, interactive=False)
591 s = r + ' <' + to_address + '>'
592 GObject.idle_add( lambda: self.payto_entry.set_text(s) )
595 thread.start_new_thread(update_status_bar_thread, ())
597 thread.start_new_thread(check_recipient_thread, ())
598 self.notebook.set_current_page(0)
600 def update_callback(self):
601 self.wallet_updated = True
603 def do_update_password(self, button, wallet):
605 show_message("No seed")
608 res = change_password_dialog(wallet.use_encryption, self.window)
610 _, password, new_password = res
613 wallet.get_seed(password)
615 show_message("Incorrect password")
618 wallet.update_password(password, new_password)
620 if wallet.use_encryption:
621 self.pw_icon.set_tooltip_text('Wallet is encrypted')
623 self.pw_icon.set_tooltip_text('Wallet is unencrypted')
626 def add_tab(self, page, name):
627 tab_label = Gtk.Label(label=name)
629 self.notebook.append_page(page, tab_label)
632 def create_send_tab(self):
634 page = vbox = Gtk.VBox()
638 payto_label = Gtk.Label(label='Pay to:')
639 payto_label.set_size_request(100,-1)
640 payto.pack_start(payto_label, False, False, 0)
641 payto_entry = Gtk.Entry()
642 payto_entry.set_size_request(450, 26)
643 payto.pack_start(payto_entry, False, False, 0)
644 vbox.pack_start(payto, False, False, 5)
647 message_label = Gtk.Label(label='Description:')
648 message_label.set_size_request(100,-1)
649 message.pack_start(message_label, False, False, 0)
650 message_entry = Gtk.Entry()
651 message_entry.set_size_request(450, 26)
652 message.pack_start(message_entry, False, False, 0)
653 vbox.pack_start(message, False, False, 5)
655 amount_box = Gtk.HBox()
656 amount_label = Gtk.Label(label='Amount:')
657 amount_label.set_size_request(100,-1)
658 amount_box.pack_start(amount_label, False, False, 0)
659 amount_entry = Gtk.Entry()
660 amount_entry.set_size_request(120, -1)
661 amount_box.pack_start(amount_entry, False, False, 0)
662 vbox.pack_start(amount_box, False, False, 5)
664 self.fee_box = fee_box = Gtk.HBox()
665 fee_label = Gtk.Label(label='Fee:')
666 fee_label.set_size_request(100,-1)
667 fee_box.pack_start(fee_label, False, False, 0)
668 fee_entry = Gtk.Entry()
669 fee_entry.set_size_request(60, 26)
670 fee_box.pack_start(fee_entry, False, False, 0)
671 vbox.pack_start(fee_box, False, False, 5)
674 empty_label = Gtk.Label(label='')
675 empty_label.set_size_request(100,-1)
676 end_box.pack_start(empty_label, False, False, 0)
677 send_button = Gtk.Button("Send")
679 end_box.pack_start(send_button, False, False, 0)
680 clear_button = Gtk.Button("Clear")
682 end_box.pack_start(clear_button, False, False, 15)
683 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
684 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
686 vbox.pack_start(end_box, False, False, 5)
688 # display this line only if there is a signature
689 payto_sig = Gtk.HBox()
690 payto_sig_id = Gtk.Label(label='')
691 payto_sig.pack_start(payto_sig_id, False, False, 0)
692 vbox.pack_start(payto_sig, True, True, 5)
695 self.user_fee = False
697 def entry_changed( entry, is_fee ):
698 self.funds_error = False
699 amount = numbify(amount_entry)
700 fee = numbify(fee_entry)
701 if not is_fee: fee = None
704 #assume two outputs - one for change
705 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, 2 )
707 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
710 amount_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#000000"))
711 fee_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#000000"))
712 send_button.set_sensitive(True)
714 send_button.set_sensitive(False)
715 amount_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#cc0000"))
716 fee_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#cc0000"))
717 self.funds_error = True
719 amount_entry.connect('changed', entry_changed, False)
720 fee_entry.connect('changed', entry_changed, True)
722 self.payto_entry = payto_entry
723 self.payto_fee_entry = fee_entry
724 self.payto_sig_id = payto_sig_id
725 self.payto_sig = payto_sig
726 self.amount_entry = amount_entry
727 self.message_entry = message_entry
728 self.add_tab(page, 'Send')
730 def set_frozen(self,entry,frozen):
732 entry.set_editable(False)
733 entry.set_has_frame(False)
734 entry.modify_base(Gtk.StateType.NORMAL, Gdk.color_parse("#eeeeee"))
736 entry.set_editable(True)
737 entry.set_has_frame(True)
738 entry.modify_base(Gtk.StateType.NORMAL, Gdk.color_parse("#ffffff"))
740 def set_url(self, url):
741 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
742 self.notebook.set_current_page(1)
743 self.payto_entry.set_text(payto)
744 self.message_entry.set_text(message)
745 self.amount_entry.set_text(amount)
747 self.set_frozen(self.payto_entry,True)
748 self.set_frozen(self.amount_entry,True)
749 self.set_frozen(self.message_entry,True)
750 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
752 self.payto_sig.set_visible(False)
754 def create_about_tab(self):
755 from gi.repository import Pango
759 tv.set_editable(False)
760 tv.set_cursor_visible(False)
761 tv.modify_font(Pango.FontDescription(MONOSPACE_FONT))
762 scroll = Gtk.ScrolledWindow()
764 page.pack_start(scroll, True, True, 0)
765 self.info = tv.get_buffer()
766 self.add_tab(page, 'Wall')
768 def do_clear(self, w, data):
769 self.payto_sig.set_visible(False)
770 self.payto_fee_entry.set_text('')
771 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
772 self.set_frozen(entry,False)
775 def question(self,msg):
776 dialog = Gtk.MessageDialog( self.window, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, msg)
778 result = dialog.run()
780 return result == Gtk.ResponseType.OK
782 def do_send(self, w, data):
783 payto_entry, label_entry, amount_entry, fee_entry = data
784 label = label_entry.get_text()
785 r = payto_entry.get_text()
788 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
789 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
792 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
796 self.update_sending_tab()
799 to_address = m2.group(5)
803 if not is_valid(to_address):
804 self.show_message( "invalid bitcoin address:\n"+to_address)
808 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
810 self.show_message( "invalid amount")
813 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
815 self.show_message( "invalid fee")
818 if self.wallet.use_encryption:
819 password = password_dialog(self.window)
826 tx = self.wallet.mktx( [(to_address, amount)], password, fee )
827 except Exception as e:
828 self.show_message(str(e))
831 if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
832 self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." )
837 self.wallet.labels[tx.hash()] = label
839 status, msg = self.wallet.sendtx( tx )
841 self.show_message( "payment sent.\n" + msg )
842 payto_entry.set_text("")
843 label_entry.set_text("")
844 amount_entry.set_text("")
845 fee_entry.set_text("")
847 self.update_sending_tab()
849 self.show_message( msg )
852 def treeview_button_press(self, treeview, event):
853 if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
854 c = treeview.get_cursor()[0]
855 if treeview == self.history_treeview:
856 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
857 self.show_message(tx_details)
858 elif treeview == self.contacts_treeview:
859 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
860 #a = self.wallet.aliases.get(m)
862 # if a[0] in self.wallet.authorities.keys():
863 # s = self.wallet.authorities.get(a[0])
866 # msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
867 # self.show_message(msg)
870 def treeview_key_press(self, treeview, event):
871 c = treeview.get_cursor()[0]
872 if event.keyval == Gdk.KEY_Up:
874 treeview.parent.grab_focus()
875 treeview.set_cursor((0,))
876 elif event.keyval == Gdk.KEY_Return:
877 if treeview == self.history_treeview:
878 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
879 self.show_message(tx_details)
880 elif treeview == self.contacts_treeview:
881 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
882 #a = self.wallet.aliases.get(m)
884 # if a[0] in self.wallet.authorities.keys():
885 # s = self.wallet.authorities.get(a[0])
888 # msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
889 # self.show_message(msg)
893 def create_history_tab(self):
895 self.history_list = Gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
896 treeview = Gtk.TreeView(model=self.history_list)
897 self.history_treeview = treeview
898 treeview.set_tooltip_column(7)
900 treeview.connect('key-press-event', self.treeview_key_press)
901 treeview.connect('button-press-event', self.treeview_button_press)
903 tvcolumn = Gtk.TreeViewColumn('')
904 treeview.append_column(tvcolumn)
905 cell = Gtk.CellRendererPixbuf()
906 tvcolumn.pack_start(cell, False)
907 tvcolumn.set_attributes(cell, stock_id=1)
909 tvcolumn = Gtk.TreeViewColumn('Date')
910 treeview.append_column(tvcolumn)
911 cell = Gtk.CellRendererText()
912 tvcolumn.pack_start(cell, False)
913 tvcolumn.add_attribute(cell, 'text', 2)
915 tvcolumn = Gtk.TreeViewColumn('Description')
916 treeview.append_column(tvcolumn)
917 cell = Gtk.CellRendererText()
918 cell.set_property('foreground', 'grey')
919 cell.set_property('family', MONOSPACE_FONT)
920 cell.set_property('editable', True)
921 def edited_cb(cell, path, new_text, h_list):
922 tx = h_list.get_value( h_list.get_iter(path), 0)
923 self.wallet.set_label(tx,new_text)
924 self.update_history_tab()
925 cell.connect('edited', edited_cb, self.history_list)
926 def editing_started(cell, entry, path, h_list):
927 tx = h_list.get_value( h_list.get_iter(path), 0)
928 if not self.wallet.labels.get(tx): entry.set_text('')
929 cell.connect('editing-started', editing_started, self.history_list)
930 tvcolumn.set_expand(True)
931 tvcolumn.pack_start(cell, True)
932 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
934 tvcolumn = Gtk.TreeViewColumn('Amount')
935 treeview.append_column(tvcolumn)
936 cell = Gtk.CellRendererText()
937 cell.set_alignment(1, 0.5)
938 cell.set_property('family', MONOSPACE_FONT)
939 tvcolumn.pack_start(cell, False)
940 tvcolumn.add_attribute(cell, 'text', 5)
942 tvcolumn = Gtk.TreeViewColumn('Balance')
943 treeview.append_column(tvcolumn)
944 cell = Gtk.CellRendererText()
945 cell.set_alignment(1, 0.5)
946 cell.set_property('family', MONOSPACE_FONT)
947 tvcolumn.pack_start(cell, False)
948 tvcolumn.add_attribute(cell, 'text', 6)
950 tvcolumn = Gtk.TreeViewColumn('Tooltip')
951 treeview.append_column(tvcolumn)
952 cell = Gtk.CellRendererText()
953 tvcolumn.pack_start(cell, False)
954 tvcolumn.add_attribute(cell, 'text', 7)
955 tvcolumn.set_visible(False)
957 scroll = Gtk.ScrolledWindow()
958 scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
961 self.add_tab(scroll, 'History')
962 self.update_history_tab()
965 def create_recv_tab(self):
966 self.recv_list = Gtk.ListStore(str, str, str, str, str)
967 self.add_tab( self.make_address_list(True), 'Receive')
968 self.update_receiving_tab()
970 def create_book_tab(self):
971 self.addressbook_list = Gtk.ListStore(str, str, str)
972 self.add_tab( self.make_address_list(False), 'Contacts')
973 self.update_sending_tab()
975 def make_address_list(self, is_recv):
976 liststore = self.recv_list if is_recv else self.addressbook_list
977 treeview = Gtk.TreeView(model= liststore)
978 treeview.connect('key-press-event', self.treeview_key_press)
979 treeview.connect('button-press-event', self.treeview_button_press)
982 self.contacts_treeview = treeview
984 tvcolumn = Gtk.TreeViewColumn('Address')
985 treeview.append_column(tvcolumn)
986 cell = Gtk.CellRendererText()
987 cell.set_property('family', MONOSPACE_FONT)
988 tvcolumn.pack_start(cell, True)
989 tvcolumn.add_attribute(cell, 'text', 0)
991 tvcolumn = Gtk.TreeViewColumn('Label')
992 tvcolumn.set_expand(True)
993 treeview.append_column(tvcolumn)
994 cell = Gtk.CellRendererText()
995 cell.set_property('editable', True)
996 def edited_cb2(cell, path, new_text, liststore):
997 address = liststore.get_value( liststore.get_iter(path), 0)
998 self.wallet.set_label(address, new_text)
999 self.update_receiving_tab()
1000 self.update_sending_tab()
1001 self.update_history_tab()
1002 cell.connect('edited', edited_cb2, liststore)
1003 tvcolumn.pack_start(cell, True)
1004 tvcolumn.add_attribute(cell, 'text', 1)
1006 tvcolumn = Gtk.TreeViewColumn('Tx')
1007 treeview.append_column(tvcolumn)
1008 cell = Gtk.CellRendererText()
1009 tvcolumn.pack_start(cell, True)
1010 tvcolumn.add_attribute(cell, 'text', 2)
1013 tvcolumn = Gtk.TreeViewColumn('Balance')
1014 treeview.append_column(tvcolumn)
1015 cell = Gtk.CellRendererText()
1016 tvcolumn.pack_start(cell, True)
1017 tvcolumn.add_attribute(cell, 'text', 3)
1018 tvcolumn = Gtk.TreeViewColumn('Type')
1019 treeview.append_column(tvcolumn)
1020 cell = Gtk.CellRendererText()
1021 tvcolumn.pack_start(cell, True)
1022 tvcolumn.add_attribute(cell, 'text', 4)
1024 scroll = Gtk.ScrolledWindow()
1025 scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
1026 scroll.add(treeview)
1030 button = Gtk.Button("New")
1031 button.connect("clicked", self.newaddress_dialog)
1033 hbox.pack_start(button,False, False, 0)
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 boxcount_row = qr.getModuleCount()
1044 size = (boxcount_row + 4) * boxsize
1045 def area_expose_cb(area, cr):
1046 style = area.get_style()
1047 Gdk.cairo_set_source_color(cr, style.white)
1048 cr.rectangle(0, 0, size, size)
1050 Gdk.cairo_set_source_color(cr, style.black)
1051 for r in range(boxcount_row):
1052 for c in range(boxcount_row):
1054 cr.rectangle((c + 2) * boxsize, (r + 2) * boxsize, boxsize, boxsize)
1056 area = Gtk.DrawingArea()
1057 area.set_size_request(size, size)
1058 area.connect("draw", area_expose_cb)
1060 dialog = Gtk.Dialog(address, parent=self.window, flags=Gtk.DialogFlags.MODAL, buttons = ("ok",1))
1061 dialog.vbox.add(area)
1065 button = Gtk.Button("QR")
1066 button.connect("clicked", showqrcode, treeview, liststore)
1068 hbox.pack_start(button,False, False, 0)
1070 button = Gtk.Button("Copy to clipboard")
1071 def copy2clipboard(w, treeview, liststore):
1073 path, col = treeview.get_cursor()
1075 address = liststore.get_value( liststore.get_iter(path), 0)
1076 if platform.system() == 'Windows':
1077 from Tkinter import Tk
1081 r.clipboard_append( address )
1084 atom = Gdk.atom_intern('CLIPBOARD', True)
1085 c = Gtk.Clipboard.get(atom)
1086 c.set_text( address, len(address) )
1087 button.connect("clicked", copy2clipboard, treeview, liststore)
1089 hbox.pack_start(button,False, False, 0)
1092 button = Gtk.Button("Freeze")
1093 def freeze_address(w, treeview, liststore, wallet):
1094 path, col = treeview.get_cursor()
1096 address = liststore.get_value( liststore.get_iter(path), 0)
1097 if address in wallet.frozen_addresses:
1098 wallet.unfreeze(address)
1100 wallet.freeze(address)
1101 self.update_receiving_tab()
1102 button.connect("clicked", freeze_address, treeview, liststore, self.wallet)
1104 hbox.pack_start(button,False, False, 0)
1107 button = Gtk.Button("Pay to")
1108 def payto(w, treeview, liststore):
1109 path, col = treeview.get_cursor()
1111 address = liststore.get_value( liststore.get_iter(path), 0)
1112 self.payto_entry.set_text( address )
1113 self.notebook.set_current_page(1)
1114 self.amount_entry.grab_focus()
1116 button.connect("clicked", payto, treeview, liststore)
1118 hbox.pack_start(button,False, False, 0)
1121 vbox.pack_start(scroll,True, True, 0)
1122 vbox.pack_start(hbox, False, False, 0)
1125 def update_status_bar(self):
1126 interface = self.network.interface
1127 if self.funds_error:
1128 text = "Not enough funds"
1129 elif interface and interface.is_connected:
1130 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height()))
1131 if not self.wallet.up_to_date:
1132 self.status_image.set_from_stock(Gtk.STOCK_REFRESH, Gtk.IconSize.MENU)
1133 text = "Synchronizing..."
1135 self.status_image.set_from_stock(Gtk.STOCK_YES, Gtk.IconSize.MENU)
1136 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height()))
1137 c, u = self.wallet.get_balance()
1138 text = "Balance: %s "%( format_satoshis(c,False,self.num_zeros) )
1139 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.num_zeros).strip() )
1141 self.status_image.set_from_stock(Gtk.STOCK_NO, Gtk.IconSize.MENU)
1142 self.network_button.set_tooltip_text("Not connected.")
1143 text = "Not connected"
1145 self.status_bar.pop(self.context_id)
1146 self.status_bar.push(self.context_id, text)
1148 if self.wallet.up_to_date and self.wallet_updated:
1149 self.update_history_tab()
1150 self.update_receiving_tab()
1151 # addressbook too...
1152 self.info.set_text( self.network.banner )
1153 self.wallet_updated = False
1155 def update_receiving_tab(self):
1156 self.recv_list.clear()
1157 for address in self.wallet.addresses(True):
1160 if self.wallet.is_change(address): Type = "C"
1161 if address in self.wallet.imported_keys.keys():
1163 c, u = self.wallet.get_addr_balance(address)
1164 if address in self.wallet.frozen_addresses: Type = Type + "F"
1165 label = self.wallet.labels.get(address)
1166 h = self.wallet.history.get(address,[])
1168 tx = "0" if n==0 else "%d"%n
1169 self.recv_list.append((address, label, tx, format_satoshis(c,False,self.num_zeros), Type ))
1171 def update_sending_tab(self):
1172 # detect addresses that are not mine in history, add them here...
1173 self.addressbook_list.clear()
1174 #for alias, v in self.wallet.aliases.items():
1176 # label = self.wallet.labels.get(alias)
1177 # self.addressbook_list.append((alias, label, '-'))
1179 for address in self.wallet.addressbook:
1180 label = self.wallet.labels.get(address)
1181 n = self.wallet.get_num_tx(address)
1182 self.addressbook_list.append((address, label, "%d"%n))
1184 def update_history_tab(self):
1185 cursor = self.history_treeview.get_cursor()[0]
1186 self.history_list.clear()
1188 for item in self.wallet.get_tx_history():
1189 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1192 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1195 conf_icon = Gtk.STOCK_APPLY
1197 time_str = 'unverified'
1200 time_str = 'pending'
1201 conf_icon = Gtk.STOCK_EXECUTE
1203 label, is_default_label = self.wallet.get_label(tx_hash)
1204 tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1205 details = self.get_tx_details(tx_hash)
1207 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1208 format_satoshis(value,True,self.num_zeros, whitespaces=True),
1209 format_satoshis(balance,False,self.num_zeros, whitespaces=True), tooltip, details] )
1210 if cursor: self.history_treeview.set_cursor( cursor )
1213 def get_tx_details(self, tx_hash):
1215 if not tx_hash: return ''
1216 tx = self.wallet.transactions.get(tx_hash)
1217 is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
1218 conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1221 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1223 time_str = 'pending'
1225 inputs = map(lambda x: x.get('address'), tx.inputs)
1226 outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1227 tx_details = "Transaction Details" +"\n\n" \
1228 + "Transaction ID:\n" + tx_hash + "\n\n" \
1229 + "Status: %d confirmations\n"%conf
1232 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1233 + "Transaction fee: %s\n"% format_satoshis(fee, False)
1235 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1236 + "Transaction fee: unknown\n"
1238 tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1240 tx_details += "Date: %s\n\n"%time_str \
1241 + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1242 + "Outputs:\n-"+ '\n-'.join(outputs)
1248 def newaddress_dialog(self, w):
1250 title = "New Contact"
1251 dialog = Gtk.Dialog(title, parent=self.window,
1252 flags=Gtk.DialogFlags.MODAL,
1253 buttons= ("cancel", 0, "ok",1) )
1257 label_label = Gtk.Label(label='Label:')
1258 label_label.set_size_request(120,10)
1260 label.pack_start(label_label, True, True, 0)
1261 label_entry = Gtk.Entry()
1263 label.pack_start(label_entry, True, True, 0)
1265 dialog.vbox.pack_start(label, False, True, 5)
1267 address = Gtk.HBox()
1268 address_label = Gtk.Label(label='Address:')
1269 address_label.set_size_request(120,10)
1270 address_label.show()
1271 address.pack_start(address_label, True, True, 0)
1272 address_entry = Gtk.Entry()
1273 address_entry.show()
1274 address.pack_start(address_entry, True, True, 0)
1276 dialog.vbox.pack_start(address, False, True, 5)
1278 result = dialog.run()
1279 address = address_entry.get_text()
1280 label = label_entry.get_text()
1284 if is_valid(address):
1285 self.wallet.add_contact(address,label)
1286 self.update_sending_tab()
1288 errorDialog = Gtk.MessageDialog(
1290 flags=Gtk.DialogFlags.MODAL,
1291 buttons= Gtk.ButtonsType.CLOSE,
1292 message_format = "Invalid address")
1295 errorDialog.destroy()
1299 class ElectrumGui():
1301 def __init__(self, config, network):
1302 self.network = network
1303 self.config = config
1306 def main(self, url=None):
1308 storage = WalletStorage(self.config)
1309 if not storage.file_exists:
1310 action = self.restore_or_create()
1313 self.wallet = wallet = Wallet(storage)
1314 gap = self.config.get('gap_limit', 5)
1316 wallet.gap_limit = gap
1317 wallet.storage.put('gap_limit', gap, True)
1320 if action == 'create':
1321 wallet.init_seed(None)
1322 show_seed_dialog(wallet, None, None)
1323 r = change_password_dialog(False, None)
1324 password = r[2] if r else None
1325 wallet.save_seed(password)
1326 wallet.synchronize() # generate first addresses offline
1328 elif action == 'restore':
1329 seed = self.seed_dialog()
1330 wallet.init_seed(seed)
1331 r = change_password_dialog(False, None)
1332 password = r[2] if r else None
1333 wallet.save_seed(password)
1338 self.wallet = Wallet(storage)
1341 self.wallet.start_threads(self.network)
1343 if action == 'restore':
1344 self.restore_wallet(wallet)
1346 w = ElectrumWindow(self.wallet, self.config, self.network)
1347 if url: w.set_url(url)
1350 def restore_or_create(self):
1351 return restore_create_dialog()
1353 def seed_dialog(self):
1354 return run_recovery_dialog()
1356 def network_dialog(self):
1357 return run_network_dialog( self.network, parent=None )
1360 def restore_wallet(self, wallet):
1362 dialog = Gtk.MessageDialog(
1364 flags = Gtk.DialogFlags.MODAL,
1365 buttons = Gtk.ButtonsType.CANCEL,
1366 message_format = "Please wait..." )
1369 def recover_thread( wallet, dialog ):
1370 wallet.restore(lambda x:x)
1371 GObject.idle_add( dialog.destroy )
1373 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1376 if r==Gtk.ResponseType.CANCEL: return False
1377 if not wallet.is_found():
1378 show_message("No transactions found for this seed")