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
26 from decimal import Decimal
28 gtk.gdk.threads_init()
31 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
33 from wallet import format_satoshis
34 from interface import DEFAULT_SERVERS
36 def numbify(entry, is_int = False):
37 text = entry.get_text().strip()
39 if not is_int: chars +='.'
40 s = ''.join([i for i in text if i in chars])
45 s = s[:p] + '.' + s[p:p+8]
47 amount = int( Decimal(s) * 100000000 )
61 def show_seed_dialog(wallet, password, parent):
64 seed = wallet.pw_decode( wallet.seed, password)
66 show_message("Incorrect password")
68 dialog = gtk.MessageDialog(
70 flags = gtk.DIALOG_MODAL,
71 buttons = gtk.BUTTONS_OK,
72 message_format = "Your wallet generation seed is:\n\n" + seed \
73 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
74 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
75 dialog.set_title("Seed")
80 def restore_create_dialog(wallet):
82 # ask if the user wants to create a new wallet, or recover from a seed.
83 # if he wants to recover, and nothing is found, do not create wallet
84 dialog = gtk.Dialog("electrum", parent=None,
85 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
86 buttons= ("create", 0, "restore",1, "cancel",2) )
88 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
90 dialog.vbox.pack_start(label)
100 if not run_network_dialog( wallet, parent=None ): return False
104 wallet.new_seed(None)
106 wallet.init_mpk( wallet.seed )
107 wallet.up_to_date_event.clear()
110 # run a dialog indicating the seed, ask the user to remember it
111 show_seed_dialog(wallet, None, None)
114 change_password_dialog(wallet, None, None)
116 # ask for seed and gap.
117 run_recovery_dialog( wallet )
119 dialog = gtk.MessageDialog(
121 flags = gtk.DIALOG_MODAL,
122 buttons = gtk.BUTTONS_CANCEL,
123 message_format = "Please wait..." )
126 def recover_thread( wallet, dialog ):
127 wallet.init_mpk( wallet.seed ) # not encrypted at this point
128 wallet.up_to_date_event.clear()
131 if wallet.is_found():
132 # history and addressbook
133 wallet.update_tx_history()
134 wallet.fill_addressbook()
135 print "recovery successful"
137 gobject.idle_add( dialog.destroy )
139 thread.start_new_thread( recover_thread, ( wallet, dialog ) )
142 if r==gtk.RESPONSE_CANCEL: return False
143 if not wallet.is_found:
144 show_message("No transactions found for this seed")
150 def run_recovery_dialog(wallet):
151 message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
152 dialog = gtk.MessageDialog(
154 flags = gtk.DIALOG_MODAL,
155 buttons = gtk.BUTTONS_OK_CANCEL,
156 message_format = message)
159 dialog.set_default_response(gtk.RESPONSE_OK)
161 # ask seed, server and gap in the same dialog
162 seed_box = gtk.HBox()
163 seed_label = gtk.Label('Seed or mnemonic:')
164 seed_label.set_size_request(150,-1)
165 seed_box.pack_start(seed_label, False, False, 10)
167 seed_entry = gtk.Entry()
169 seed_entry.set_size_request(450,-1)
170 seed_box.pack_start(seed_entry, False, False, 10)
171 add_help_button(seed_box, '.')
173 vbox.pack_start(seed_box, False, False, 5)
176 gap_label = gtk.Label('Gap limit:')
177 gap_label.set_size_request(150,10)
179 gap.pack_start(gap_label,False, False, 10)
180 gap_entry = gtk.Entry()
181 gap_entry.set_text("%d"%wallet.gap_limit)
182 gap_entry.connect('changed', numbify, True)
184 gap.pack_start(gap_entry,False,False, 10)
185 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.')
187 vbox.pack_start(gap, False,False, 5)
191 gap = gap_entry.get_text()
192 seed = seed_entry.get_text()
195 if r==gtk.RESPONSE_CANCEL:
200 show_message("error")
207 print "not hex, trying decode"
208 seed = mnemonic.mn_decode( seed.split(' ') )
210 show_message("no seed")
214 wallet.gap_limit = gap
219 def run_settings_dialog(wallet, parent):
221 message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
223 dialog = gtk.MessageDialog(
225 flags = gtk.DIALOG_MODAL,
226 buttons = gtk.BUTTONS_OK_CANCEL,
227 message_format = message)
230 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
232 dialog.set_image(image)
233 dialog.set_title("Settings")
236 dialog.set_default_response(gtk.RESPONSE_OK)
239 fee_entry = gtk.Entry()
240 fee_label = gtk.Label('Transaction fee:')
241 fee_label.set_size_request(150,10)
243 fee.pack_start(fee_label,False, False, 10)
244 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
245 fee_entry.connect('changed', numbify, False)
247 fee.pack_start(fee_entry,False,False, 10)
248 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
250 vbox.pack_start(fee, False,False, 5)
254 fee = fee_entry.get_text()
257 if r==gtk.RESPONSE_CANCEL:
261 fee = int( 100000000 * Decimal(fee) )
263 show_message("error")
272 def run_network_dialog( wallet, parent ):
274 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
275 interface = wallet.interface
277 if interface.is_connected:
278 status = "Connected to %s:%d\n%d blocks\nresponse time: %f"%(interface.host, interface.port, wallet.blocks, interface.rtime)
280 status = "Not connected"
281 server = wallet.server
284 status = "Please choose a server."
285 server = random.choice( DEFAULT_SERVERS )
288 for item in wallet.interface.servers:
292 protocol, port = item2
296 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
297 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
298 dialog.set_title("Server")
299 dialog.set_image(image)
303 host_box = gtk.HBox()
304 host_label = gtk.Label('Connect to:')
305 host_label.set_size_request(100,-1)
307 host_box.pack_start(host_label, False, False, 10)
308 host_entry = gtk.Entry()
309 host_entry.set_size_request(200,-1)
310 host_entry.set_text(server)
312 host_box.pack_start(host_entry, False, False, 10)
313 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)')
317 p_box = gtk.HBox(False, 10)
320 p_label = gtk.Label('Protocol:')
321 p_label.set_size_request(100,-1)
323 p_box.pack_start(p_label, False, False, 10)
325 radio1 = gtk.RadioButton(None, "tcp")
326 p_box.pack_start(radio1, True, True, 0)
328 radio2 = gtk.RadioButton(radio1, "http")
329 p_box.pack_start(radio2, True, True, 0)
331 radio3 = gtk.RadioButton(radio1, "native")
332 p_box.pack_start(radio3, True, True, 0)
336 return unicode(host_entry.get_text()).split(':')
338 def set_button(protocol):
341 elif protocol == 'h':
343 elif protocol == 'n':
346 def set_protocol(protocol):
347 host = current_line()[0]
349 if protocol not in pp.keys():
350 protocol = pp.keys()[0]
353 host_entry.set_text( host + ':' + port + ':' + protocol)
355 radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
356 radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
357 radio3.connect("toggled", lambda x,y:set_protocol('n'), "radio button 1")
359 server_list = gtk.ListStore(str)
360 for host in plist.keys():
361 server_list.append([host])
363 treeview = gtk.TreeView(model=server_list)
366 tvcolumn = gtk.TreeViewColumn('Active servers')
367 treeview.append_column(tvcolumn)
368 cell = gtk.CellRendererText()
369 tvcolumn.pack_start(cell, False)
370 tvcolumn.add_attribute(cell, 'text', 0)
372 scroll = gtk.ScrolledWindow()
373 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
377 vbox.pack_start(host_box, False,False, 5)
378 vbox.pack_start(p_box, True, True, 0)
379 vbox.pack_start(scroll)
381 def my_treeview_cb(treeview):
382 path, view_column = treeview.get_cursor()
383 host = server_list.get_value( server_list.get_iter(path), 0)
389 protocol = pp.keys()[0]
391 host_entry.set_text( host + ':' + port + ':' + protocol)
394 treeview.connect('cursor-changed', my_treeview_cb)
398 server = host_entry.get_text()
401 if r==gtk.RESPONSE_CANCEL:
405 wallet.set_server(server)
407 show_message("error:" + server)
416 def show_message(message, parent=None):
417 dialog = gtk.MessageDialog(
419 flags = gtk.DIALOG_MODAL,
420 buttons = gtk.BUTTONS_CLOSE,
421 message_format = message )
426 def password_line(label):
427 password = gtk.HBox()
428 password_label = gtk.Label(label)
429 password_label.set_size_request(120,10)
430 password_label.show()
431 password.pack_start(password_label,False, False, 10)
432 password_entry = gtk.Entry()
433 password_entry.set_size_request(300,-1)
434 password_entry.set_visibility(False)
435 password_entry.show()
436 password.pack_start(password_entry,False,False, 10)
438 return password, password_entry
440 def password_dialog(parent):
441 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
442 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
443 dialog.get_image().set_visible(False)
444 current_pw, current_pw_entry = password_line('Password:')
445 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
446 dialog.vbox.pack_start(current_pw, False, True, 0)
448 result = dialog.run()
449 pw = current_pw_entry.get_text()
451 if result != gtk.RESPONSE_CANCEL: return pw
453 def change_password_dialog(wallet, parent, icon):
455 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'
457 msg = "Please choose a password to encrypt your wallet keys"
459 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
460 dialog.set_title("Change password")
462 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
464 dialog.set_image(image)
466 if wallet.use_encryption:
467 current_pw, current_pw_entry = password_line('Current password:')
468 dialog.vbox.pack_start(current_pw, False, True, 0)
470 password, password_entry = password_line('New password:')
471 dialog.vbox.pack_start(password, False, True, 5)
472 password2, password2_entry = password_line('Confirm password:')
473 dialog.vbox.pack_start(password2, False, True, 5)
476 result = dialog.run()
477 password = current_pw_entry.get_text() if wallet.use_encryption else None
478 new_password = password_entry.get_text()
479 new_password2 = password2_entry.get_text()
481 if result == gtk.RESPONSE_CANCEL:
485 seed = wallet.pw_decode( wallet.seed, password)
487 show_message("Incorrect password")
490 if new_password != new_password2:
491 show_message("passwords do not match")
494 wallet.update_password(seed, new_password)
497 if wallet.use_encryption:
498 icon.set_tooltip_text('wallet is encrypted')
500 icon.set_tooltip_text('wallet is unencrypted')
503 def add_help_button(hbox, message):
504 button = gtk.Button('?')
505 button.connect("clicked", lambda x: show_message(message))
507 hbox.pack_start(button,False, False)
510 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
512 gobject.type_register(MyWindow)
513 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
514 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
517 class ElectrumWindow:
519 def show_message(self, msg):
520 show_message(msg, self.window)
522 def __init__(self, wallet):
524 self.funds_error = False # True if not enough funds
526 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
527 self.window.set_title(APP_NAME + " " + self.wallet.electrum_version)
528 self.window.connect("destroy", gtk.main_quit)
529 self.window.set_border_width(0)
530 self.window.connect('mykeypress', gtk.main_quit)
531 self.window.set_default_size(720, 350)
535 self.notebook = gtk.Notebook()
536 self.create_history_tab()
537 self.create_send_tab()
538 self.create_recv_tab()
539 self.create_book_tab()
540 self.create_about_tab()
542 vbox.pack_start(self.notebook, True, True, 2)
544 self.status_bar = gtk.Statusbar()
545 vbox.pack_start(self.status_bar, False, False, 0)
547 self.status_image = gtk.Image()
548 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
549 self.status_image.set_alignment(True, 0.5 )
550 self.status_image.show()
552 self.network_button = gtk.Button()
553 self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
554 self.network_button.add(self.status_image)
555 self.network_button.set_relief(gtk.RELIEF_NONE)
556 self.network_button.show()
557 self.status_bar.pack_end(self.network_button, False, False)
559 def seedb(w, wallet):
560 if wallet.use_encryption:
561 password = password_dialog(self.window)
562 if not password: return
563 else: password = None
564 show_seed_dialog(wallet, password, self.window)
565 button = gtk.Button('S')
566 button.connect("clicked", seedb, wallet )
567 button.set_relief(gtk.RELIEF_NONE)
569 self.status_bar.pack_end(button,False, False)
571 settings_icon = gtk.Image()
572 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
573 settings_icon.set_alignment(0.5, 0.5)
574 settings_icon.set_size_request(16,16 )
577 prefs_button = gtk.Button()
578 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
579 prefs_button.add(settings_icon)
580 prefs_button.set_tooltip_text("Settings")
581 prefs_button.set_relief(gtk.RELIEF_NONE)
583 self.status_bar.pack_end(prefs_button,False,False)
585 pw_icon = gtk.Image()
586 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
587 pw_icon.set_alignment(0.5, 0.5)
588 pw_icon.set_size_request(16,16 )
591 password_button = gtk.Button()
592 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
593 password_button.add(pw_icon)
594 password_button.set_relief(gtk.RELIEF_NONE)
595 password_button.show()
596 self.status_bar.pack_end(password_button,False,False)
598 self.window.add(vbox)
599 self.window.show_all()
602 self.context_id = self.status_bar.get_context_id("statusbar")
603 self.update_status_bar()
605 def update_status_bar_thread():
607 gobject.idle_add( self.update_status_bar )
611 def check_recipient_thread():
615 if self.payto_entry.is_focus():
617 r = self.payto_entry.get_text()
621 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
623 to_address = self.wallet.get_alias(r, interactive=False)
627 s = r + ' <' + to_address + '>'
628 gobject.idle_add( lambda: self.payto_entry.set_text(s) )
631 thread.start_new_thread(update_status_bar_thread, ())
632 thread.start_new_thread(check_recipient_thread, ())
633 self.notebook.set_current_page(0)
636 def add_tab(self, page, name):
637 tab_label = gtk.Label(name)
639 self.notebook.append_page(page, tab_label)
642 def create_send_tab(self):
644 page = vbox = gtk.VBox()
648 payto_label = gtk.Label('Pay to:')
649 payto_label.set_size_request(100,-1)
650 payto.pack_start(payto_label, False)
651 payto_entry = gtk.Entry()
652 payto_entry.set_size_request(450, 26)
653 payto.pack_start(payto_entry, False)
654 vbox.pack_start(payto, False, False, 5)
657 message_label = gtk.Label('Description:')
658 message_label.set_size_request(100,-1)
659 message.pack_start(message_label, False)
660 message_entry = gtk.Entry()
661 message_entry.set_size_request(450, 26)
662 message.pack_start(message_entry, False)
663 vbox.pack_start(message, False, False, 5)
665 amount_box = gtk.HBox()
666 amount_label = gtk.Label('Amount:')
667 amount_label.set_size_request(100,-1)
668 amount_box.pack_start(amount_label, False)
669 amount_entry = gtk.Entry()
670 amount_entry.set_size_request(120, -1)
671 amount_box.pack_start(amount_entry, False)
672 vbox.pack_start(amount_box, False, False, 5)
674 self.fee_box = fee_box = gtk.HBox()
675 fee_label = gtk.Label('Fee:')
676 fee_label.set_size_request(100,-1)
677 fee_box.pack_start(fee_label, False)
678 fee_entry = gtk.Entry()
679 fee_entry.set_size_request(60, 26)
680 fee_box.pack_start(fee_entry, False)
681 vbox.pack_start(fee_box, False, False, 5)
684 empty_label = gtk.Label('')
685 empty_label.set_size_request(100,-1)
686 end_box.pack_start(empty_label, False)
687 send_button = gtk.Button("Send")
689 end_box.pack_start(send_button, False, False, 0)
690 clear_button = gtk.Button("Clear")
692 end_box.pack_start(clear_button, False, False, 15)
693 send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
694 clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
696 vbox.pack_start(end_box, False, False, 5)
698 # display this line only if there is a signature
699 payto_sig = gtk.HBox()
700 payto_sig_id = gtk.Label('')
701 payto_sig.pack_start(payto_sig_id, False)
702 vbox.pack_start(payto_sig, True, True, 5)
705 self.user_fee = False
707 def entry_changed( entry, is_fee ):
708 self.funds_error = False
709 amount = numbify(amount_entry)
710 fee = numbify(fee_entry)
711 if not is_fee: fee = None
714 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
716 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
719 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
720 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
721 send_button.set_sensitive(True)
723 send_button.set_sensitive(False)
724 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
725 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
726 self.funds_error = True
728 amount_entry.connect('changed', entry_changed, False)
729 fee_entry.connect('changed', entry_changed, True)
731 self.payto_entry = payto_entry
732 self.payto_fee_entry = fee_entry
733 self.payto_sig_id = payto_sig_id
734 self.payto_sig = payto_sig
735 self.amount_entry = amount_entry
736 self.message_entry = message_entry
737 self.add_tab(page, 'Send')
739 def set_frozen(self,entry,frozen):
741 entry.set_editable(False)
742 entry.set_has_frame(False)
743 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
745 entry.set_editable(True)
746 entry.set_has_frame(True)
747 entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
749 def set_url(self, url):
750 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
751 self.notebook.set_current_page(1)
752 self.payto_entry.set_text(payto)
753 self.message_entry.set_text(message)
754 self.amount_entry.set_text(amount)
756 self.set_frozen(self.payto_entry,True)
757 self.set_frozen(self.amount_entry,True)
758 self.set_frozen(self.message_entry,True)
759 self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
761 self.payto_sig.set_visible(False)
763 def create_about_tab(self):
768 tv.set_editable(False)
769 tv.set_cursor_visible(False)
770 tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
772 self.info = tv.get_buffer()
773 self.add_tab(page, 'Wall')
775 def do_clear(self, w, data):
776 self.payto_sig.set_visible(False)
777 self.payto_fee_entry.set_text('')
778 for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
779 self.set_frozen(entry,False)
782 def question(self,msg):
783 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
785 result = dialog.run()
787 return result == gtk.RESPONSE_OK
789 def do_send(self, w, data):
790 payto_entry, label_entry, amount_entry, fee_entry = data
791 label = label_entry.get_text()
792 r = payto_entry.get_text()
795 m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
796 m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
799 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
803 self.update_sending_tab()
806 to_address = m2.group(5)
810 if not self.wallet.is_valid(to_address):
811 self.show_message( "invalid bitcoin address:\n"+to_address)
815 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
817 self.show_message( "invalid amount")
820 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
822 self.show_message( "invalid fee")
825 if self.wallet.use_encryption:
826 password = password_dialog(self.window)
833 tx = self.wallet.mktx( to_address, amount, label, password, fee )
834 except BaseException, e:
835 self.show_message(e.message)
838 status, msg = self.wallet.sendtx( tx )
840 self.show_message( "payment sent.\n" + msg )
841 payto_entry.set_text("")
842 label_entry.set_text("")
843 amount_entry.set_text("")
844 fee_entry.set_text("")
846 self.update_sending_tab()
848 self.show_message( msg )
851 def treeview_button_press(self, treeview, event):
852 if event.type == gtk.gdk._2BUTTON_PRESS:
853 c = treeview.get_cursor()[0]
854 if treeview == self.history_treeview:
855 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
856 self.show_message(tx_details)
857 elif treeview == self.contacts_treeview:
858 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
859 a = self.wallet.aliases.get(m)
861 if a[0] in self.wallet.authorities.keys():
862 s = self.wallet.authorities.get(a[0])
865 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
866 self.show_message(msg)
869 def treeview_key_press(self, treeview, event):
870 c = treeview.get_cursor()[0]
871 if event.keyval == gtk.keysyms.Up:
873 treeview.parent.grab_focus()
874 treeview.set_cursor((0,))
875 elif event.keyval == gtk.keysyms.Return:
876 if treeview == self.history_treeview:
877 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
878 self.show_message(tx_details)
879 elif treeview == self.contacts_treeview:
880 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
881 a = self.wallet.aliases.get(m)
883 if a[0] in self.wallet.authorities.keys():
884 s = self.wallet.authorities.get(a[0])
887 msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
888 self.show_message(msg)
892 def create_history_tab(self):
894 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
895 treeview = gtk.TreeView(model=self.history_list)
896 self.history_treeview = treeview
897 treeview.set_tooltip_column(7)
899 treeview.connect('key-press-event', self.treeview_key_press)
900 treeview.connect('button-press-event', self.treeview_button_press)
902 tvcolumn = gtk.TreeViewColumn('')
903 treeview.append_column(tvcolumn)
904 cell = gtk.CellRendererPixbuf()
905 tvcolumn.pack_start(cell, False)
906 tvcolumn.set_attributes(cell, stock_id=1)
908 tvcolumn = gtk.TreeViewColumn('Date')
909 treeview.append_column(tvcolumn)
910 cell = gtk.CellRendererText()
911 tvcolumn.pack_start(cell, False)
912 tvcolumn.add_attribute(cell, 'text', 2)
914 tvcolumn = gtk.TreeViewColumn('Description')
915 treeview.append_column(tvcolumn)
916 cell = gtk.CellRendererText()
917 cell.set_property('foreground', 'grey')
918 cell.set_property('family', MONOSPACE_FONT)
919 cell.set_property('editable', True)
920 def edited_cb(cell, path, new_text, h_list):
921 tx = h_list.get_value( h_list.get_iter(path), 0)
922 self.wallet.labels[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.POLICY_NEVER, gtk.POLICY_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)
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.labels[address] = new_text
1000 self.wallet.update_tx_labels()
1001 self.update_receiving_tab()
1002 self.update_sending_tab()
1003 self.update_history_tab()
1004 cell.connect('edited', edited_cb2, liststore)
1005 tvcolumn.pack_start(cell, True)
1006 tvcolumn.add_attribute(cell, 'text', 1)
1008 tvcolumn = gtk.TreeViewColumn('Tx')
1009 treeview.append_column(tvcolumn)
1010 cell = gtk.CellRendererText()
1011 tvcolumn.pack_start(cell, True)
1012 tvcolumn.add_attribute(cell, 'text', 2)
1014 scroll = gtk.ScrolledWindow()
1015 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1016 scroll.add(treeview)
1020 button = gtk.Button("New")
1021 button.connect("clicked", self.newaddress_dialog)
1023 hbox.pack_start(button,False)
1025 def showqrcode(w, treeview, liststore):
1026 path, col = treeview.get_cursor()
1028 address = liststore.get_value(liststore.get_iter(path), 0)
1029 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1033 size = qr.getModuleCount()*boxsize
1034 def area_expose_cb(area, event):
1035 style = area.get_style()
1036 k = qr.getModuleCount()
1039 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1040 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1041 area = gtk.DrawingArea()
1042 area.set_size_request(size, size)
1043 area.connect("expose-event", area_expose_cb)
1045 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1046 dialog.vbox.add(area)
1050 button = gtk.Button("QR")
1051 button.connect("clicked", showqrcode, treeview, liststore)
1053 hbox.pack_start(button,False)
1055 button = gtk.Button("Copy to clipboard")
1056 def copy2clipboard(w, treeview, liststore):
1058 path, col = treeview.get_cursor()
1060 address = liststore.get_value( liststore.get_iter(path), 0)
1061 if platform.system() == 'Windows':
1062 from Tkinter import Tk
1066 r.clipboard_append( address )
1069 c = gtk.clipboard_get()
1070 c.set_text( address )
1071 button.connect("clicked", copy2clipboard, treeview, liststore)
1073 hbox.pack_start(button,False)
1076 button = gtk.Button("Pay to")
1077 def payto(w, treeview, liststore):
1078 path, col = treeview.get_cursor()
1080 address = liststore.get_value( liststore.get_iter(path), 0)
1081 self.payto_entry.set_text( address )
1082 self.notebook.set_current_page(1)
1083 self.amount_entry.grab_focus()
1085 button.connect("clicked", payto, treeview, liststore)
1087 hbox.pack_start(button,False)
1090 vbox.pack_start(scroll,True)
1091 vbox.pack_start(hbox, False)
1094 def update_status_bar(self):
1095 interface = self.wallet.interface
1096 if self.funds_error:
1097 text = "Not enough funds"
1098 elif interface.is_connected:
1099 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks\nresponse time: %f"%(interface.host, interface.port, self.wallet.blocks, interface.rtime))
1100 if self.wallet.blocks == -1:
1101 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1102 text = "Connecting..."
1103 elif self.wallet.blocks == 0:
1104 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1105 text = "Server not ready"
1106 elif not self.wallet.up_to_date:
1107 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1108 text = "Synchronizing..."
1110 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1111 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks\nresponse time: %f"%(interface.host, interface.port, self.wallet.blocks, interface.rtime))
1112 c, u = self.wallet.get_balance()
1113 text = "Balance: %s "%( format_satoshis(c) )
1114 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True).strip() )
1116 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1117 self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(interface.host, self.wallet.blocks))
1118 text = "Not connected"
1120 self.status_bar.pop(self.context_id)
1121 self.status_bar.push(self.context_id, text)
1123 if self.wallet.was_updated and self.wallet.up_to_date:
1124 self.update_history_tab()
1125 self.update_receiving_tab()
1126 # addressbook too...
1127 self.info.set_text( self.wallet.banner )
1128 self.wallet.was_updated = False
1131 def update_receiving_tab(self):
1132 self.recv_list.clear()
1133 for address in self.wallet.all_addresses():
1134 if self.wallet.is_change(address):continue
1135 label = self.wallet.labels.get(address)
1137 h = self.wallet.history.get(address,[])
1139 if not item['is_input'] : n=n+1
1140 tx = "None" if n==0 else "%d"%n
1141 self.recv_list.append((address, label, tx ))
1143 def update_sending_tab(self):
1144 # detect addresses that are not mine in history, add them here...
1145 self.addressbook_list.clear()
1146 for alias, v in self.wallet.aliases.items():
1148 label = self.wallet.labels.get(alias)
1149 self.addressbook_list.append((alias, label, '-'))
1151 for address in self.wallet.addressbook:
1152 label = self.wallet.labels.get(address)
1154 for item in self.wallet.tx_history.values():
1155 if address in item['outputs'] : n=n+1
1156 tx = "None" if n==0 else "%d"%n
1157 self.addressbook_list.append((address, label, tx))
1159 def update_history_tab(self):
1160 cursor = self.history_treeview.get_cursor()[0]
1161 self.history_list.clear()
1163 for tx in self.wallet.get_tx_history():
1164 tx_hash = tx['tx_hash']
1166 conf = self.wallet.blocks - tx['height'] + 1
1167 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
1168 conf_icon = gtk.STOCK_APPLY
1171 time_str = 'pending'
1172 conf_icon = gtk.STOCK_EXECUTE
1175 label = self.wallet.labels.get(tx_hash)
1176 is_default_label = (label == '') or (label is None)
1177 if is_default_label: label = tx['default_label']
1178 tooltip = tx_hash + "\n%d confirmations"%conf
1180 # tx = self.wallet.tx_history.get(tx_hash)
1181 details = "Transaction Details:\n\n" \
1182 + "Transaction ID:\n" + tx_hash + "\n\n" \
1183 + "Status: %d confirmations\n\n"%conf \
1184 + "Date: %s\n\n"%time_str \
1185 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
1186 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
1187 r = self.wallet.receipts.get(tx_hash)
1189 details += "\n_______________________________________" \
1190 + '\n\nSigned URI: ' + r[2] \
1191 + "\n\nSigned by: " + r[0] \
1192 + '\n\nSignature: ' + r[1]
1195 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1196 format_satoshis(v,True), format_satoshis(balance), tooltip, details] )
1197 if cursor: self.history_treeview.set_cursor( cursor )
1201 def newaddress_dialog(self, w):
1203 title = "New Contact"
1204 dialog = gtk.Dialog(title, parent=self.window,
1205 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1206 buttons= ("cancel", 0, "ok",1) )
1210 label_label = gtk.Label('Label:')
1211 label_label.set_size_request(120,10)
1213 label.pack_start(label_label)
1214 label_entry = gtk.Entry()
1216 label.pack_start(label_entry)
1218 dialog.vbox.pack_start(label, False, True, 5)
1220 address = gtk.HBox()
1221 address_label = gtk.Label('Address:')
1222 address_label.set_size_request(120,10)
1223 address_label.show()
1224 address.pack_start(address_label)
1225 address_entry = gtk.Entry()
1226 address_entry.show()
1227 address.pack_start(address_entry)
1229 dialog.vbox.pack_start(address, False, True, 5)
1231 result = dialog.run()
1232 address = address_entry.get_text()
1233 label = label_entry.get_text()
1237 if self.wallet.is_valid(address):
1238 self.wallet.addressbook.append(address)
1239 if label: self.wallet.labels[address] = label
1241 self.update_sending_tab()
1243 errorDialog = gtk.MessageDialog(
1245 flags=gtk.DIALOG_MODAL,
1246 buttons= gtk.BUTTONS_CLOSE,
1247 message_format = "Invalid address")
1250 errorDialog.destroy()
1254 class ElectrumGui():
1256 def __init__(self, wallet):
1257 self.wallet = wallet
1259 def main(self, url=None):
1260 ew = ElectrumWindow(self.wallet)
1261 if url: ew.set_url(url)
1264 def restore_or_create(self):
1265 return restore_create_dialog(self.wallet)