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
21 import socket, traceback
26 from decimal import Decimal
28 gtk.gdk.threads_init()
31 def format_satoshis(x):
32 xx = str( Decimal(x) /100000000 )
33 #xx = ("%f"%(x*1e-8)).rstrip('0')
34 if not '.' in xx: xx = xx + '.'
35 if len(xx) > 0 and xx[-1] =='.': xx+="00"
36 if len(xx) > 1 and xx[-2] =='.': xx+="0"
39 def numbify(entry, is_int = False):
40 text = entry.get_text().strip()
41 s = ''.join([i for i in text if i in '0123456789.'])
46 amount = int( Decimal(s) * 100000000 )
55 def show_seed_dialog(wallet, password, parent):
58 seed = wallet.pw_decode( wallet.seed, password)
59 private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
61 show_message("Incorrect password")
63 dialog = gtk.MessageDialog(
65 flags = gtk.DIALOG_MODAL,
66 buttons = gtk.BUTTONS_OK,
67 message_format = "Your wallet generation seed is:\n\n" + seed \
68 + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
69 + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
70 dialog.set_title("Seed")
75 def init_wallet(wallet):
79 except BaseException, e:
80 show_message(e.message)
84 # ask if the user wants to create a new wallet, or recover from a seed.
85 # if he wants to recover, and nothing is found, do not create wallet
86 dialog = gtk.Dialog("electrum", parent=None,
87 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
88 buttons= ("create", 0, "restore",1, "cancel",2) )
90 label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
92 dialog.vbox.pack_start(label)
103 wallet.new_seed(None)
105 # ask for the server.
106 run_settings_dialog(wallet, is_create=True, is_recovery=False, parent=None)
109 wallet.create_new_address(False, None)
111 # run a dialog indicating the seed, ask the user to remember it
112 show_seed_dialog(wallet, None, None)
115 change_password_dialog(wallet, None, None)
118 # ask for the server, seed and gap.
119 run_settings_dialog(wallet, is_create=True, is_recovery=True, parent=None)
121 dialog = gtk.MessageDialog(
123 flags = gtk.DIALOG_MODAL,
124 buttons = gtk.BUTTONS_CANCEL,
125 message_format = "Please wait..." )
128 def recover_thread( wallet, dialog, password ):
129 wallet.is_found = wallet.recover( password )
132 gobject.idle_add( dialog.destroy )
134 thread.start_new_thread( recover_thread, ( wallet, dialog, None ) ) # no password
137 if r==gtk.RESPONSE_CANCEL: sys.exit(1)
138 if not wallet.is_found:
139 show_message("No transactions found for this seed")
142 def run_settings_dialog(wallet, is_create, is_recovery, parent):
145 message = "Please enter your wallet seed or the corresponding mnemonic list of words, the server and the gap limit"
147 message = "Please indicate the server and port number"
149 message = "These are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
151 dialog = gtk.MessageDialog(
153 flags = gtk.DIALOG_MODAL,
154 buttons = gtk.BUTTONS_OK_CANCEL,
155 message_format = message)
158 image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
160 dialog.set_image(image)
161 dialog.set_title("Settings")
164 dialog.set_default_response(gtk.RESPONSE_OK)
167 # ask seed, server and gap in the same dialog
168 seed_box = gtk.HBox()
169 seed_label = gtk.Label('Seed or mnemonic:')
170 seed_label.set_size_request(150,-1)
171 seed_box.pack_start(seed_label, False, False, 10)
173 seed_entry = gtk.Entry()
175 seed_entry.set_size_request(450,-1)
176 seed_box.pack_start(seed_entry, False, False, 10)
177 add_help_button(seed_box, '.')
179 vbox.pack_start(seed_box, False, False, 5)
181 if is_recovery or (not is_create):
183 gap_label = gtk.Label('Gap limit:')
184 gap_label.set_size_request(150,10)
186 gap.pack_start(gap_label,False, False, 10)
187 gap_entry = gtk.Entry()
188 gap_entry.set_text("%d"%wallet.gap_limit)
189 gap_entry.connect('changed', numbify, True)
191 gap.pack_start(gap_entry,False,False, 10)
192 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.')
194 vbox.pack_start(gap, False,False, 5)
196 if is_recovery or is_create:
198 host_label = gtk.Label('Server:')
199 host_label.set_size_request(150,-1)
201 host.pack_start(host_label,False, False, 10)
202 host_entry = gtk.Entry()
203 host_entry.set_text(wallet.host+":%d"%wallet.port)
205 host.pack_start(host_entry,False,False, 10)
206 add_help_button(host, 'The name and port number of your Electrum server, separated by a colon. Example: "ecdsa.org:50000". If no port number is provided, the http port 80 will be tried.')
208 vbox.pack_start(host, False,False, 5)
212 fee_entry = gtk.Entry()
213 fee_label = gtk.Label('Transaction fee:')
214 fee_label.set_size_request(150,10)
216 fee.pack_start(fee_label,False, False, 10)
217 fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
218 fee_entry.connect('changed', numbify, False)
220 fee.pack_start(fee_entry,False,False, 10)
221 add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
223 vbox.pack_start(fee, False,False, 5)
229 hh = host_entry.get_text()
231 gap = gap_entry.get_text()
232 seed = seed_entry.get_text()
237 print "not hex, trying decode"
238 seed = mnemonic.mn_decode( seed.split(' ') )
240 fee = fee_entry.get_text()
241 gap = gap_entry.get_text()
244 if r==gtk.RESPONSE_CANCEL:
245 if is_create: sys.exit(1)
251 host, port = hh.split(':')
259 fee = int( 100000000 * Decimal(fee) )
262 show_message("error")
270 wallet.gap_limit = gap
273 wallet.gap_limit = gap
279 def show_message(message, parent=None):
280 dialog = gtk.MessageDialog(
282 flags = gtk.DIALOG_MODAL,
283 buttons = gtk.BUTTONS_CLOSE,
284 message_format = message )
289 def password_line(label):
290 password = gtk.HBox()
291 password_label = gtk.Label(label)
292 password_label.set_size_request(120,10)
293 password_label.show()
294 password.pack_start(password_label,False, False, 10)
295 password_entry = gtk.Entry()
296 password_entry.set_size_request(300,-1)
297 password_entry.set_visibility(False)
298 password_entry.show()
299 password.pack_start(password_entry,False,False, 10)
301 return password, password_entry
303 def password_dialog():
304 dialog = gtk.MessageDialog( None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
305 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
306 dialog.get_image().set_visible(False)
307 current_pw, current_pw_entry = password_line('Password:')
308 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
309 dialog.vbox.pack_start(current_pw, False, True, 0)
311 result = dialog.run()
312 pw = current_pw_entry.get_text()
316 def change_password_dialog(wallet, parent, icon):
318 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'
320 msg = "Please choose a password to encrypt your wallet keys"
322 dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
323 dialog.set_title("Change password")
325 image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
327 dialog.set_image(image)
329 if wallet.use_encryption:
330 current_pw, current_pw_entry = password_line('Current password:')
331 dialog.vbox.pack_start(current_pw, False, True, 0)
333 password, password_entry = password_line('New password:')
334 dialog.vbox.pack_start(password, False, True, 5)
335 password2, password2_entry = password_line('Confirm password:')
336 dialog.vbox.pack_start(password2, False, True, 5)
339 result = dialog.run()
340 password = current_pw_entry.get_text() if wallet.use_encryption else None
341 new_password = password_entry.get_text()
342 new_password2 = password2_entry.get_text()
344 if result == gtk.RESPONSE_CANCEL:
348 seed = wallet.pw_decode( wallet.seed, password)
349 private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
351 show_message("Incorrect password")
354 if new_password != new_password2:
355 show_message("passwords do not match")
358 wallet.use_encryption = (new_password != '')
359 wallet.seed = wallet.pw_encode( seed, new_password)
360 wallet.private_keys = wallet.pw_encode( repr( private_keys ), new_password)
364 if wallet.use_encryption:
365 icon.set_tooltip_text('wallet is encrypted')
367 icon.set_tooltip_text('wallet is unencrypted')
370 def add_help_button(hbox, message):
371 button = gtk.Button('?')
372 button.connect("clicked", lambda x: show_message(message))
374 hbox.pack_start(button,False, False)
377 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
379 gobject.type_register(MyWindow)
380 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
381 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
386 def show_message(self, msg):
387 show_message(msg, self.window)
389 def __init__(self, wallet):
391 self.is_connected = False
395 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
396 self.window.set_title(APP_NAME)
397 self.window.connect("destroy", gtk.main_quit)
398 self.window.set_border_width(0)
399 self.window.connect('mykeypress', gtk.main_quit)
400 self.window.set_default_size(720, 350)
404 self.notebook = gtk.Notebook()
405 self.create_history_tab()
406 self.create_send_tab()
407 self.create_recv_tab()
408 self.create_book_tab()
409 self.create_about_tab()
411 vbox.pack_start(self.notebook, True, True, 2)
413 self.status_bar = gtk.Statusbar()
414 vbox.pack_start(self.status_bar, False, False, 0)
416 self.status_image = gtk.Image()
417 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
418 self.status_image.set_alignment(True, 0.5 )
419 self.status_image.show()
421 self.network_button = gtk.Button()
422 self.network_button.connect("clicked", self.network_dialog )
423 self.network_button.add(self.status_image)
424 self.network_button.set_relief(gtk.RELIEF_NONE)
425 self.network_button.show()
426 self.status_bar.pack_end(self.network_button, False, False)
428 def seedb(w, wallet):
429 if wallet.use_encryption:
430 password = password_dialog()
431 if not password: return
432 else: password = None
433 show_seed_dialog(wallet, password, self.window)
434 button = gtk.Button('S')
435 button.connect("clicked", seedb, wallet )
436 button.set_relief(gtk.RELIEF_NONE)
438 self.status_bar.pack_end(button,False, False)
440 settings_icon = gtk.Image()
441 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
442 settings_icon.set_alignment(0.5, 0.5)
443 settings_icon.set_size_request(16,16 )
446 prefs_button = gtk.Button()
447 prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, False, False, self.window) )
448 prefs_button.add(settings_icon)
449 prefs_button.set_tooltip_text("Settings")
450 prefs_button.set_relief(gtk.RELIEF_NONE)
452 self.status_bar.pack_end(prefs_button,False,False)
454 pw_icon = gtk.Image()
455 pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
456 pw_icon.set_alignment(0.5, 0.5)
457 pw_icon.set_size_request(16,16 )
460 password_button = gtk.Button()
461 password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
462 password_button.add(pw_icon)
463 password_button.set_relief(gtk.RELIEF_NONE)
464 password_button.show()
465 self.status_bar.pack_end(password_button,False,False)
467 self.window.add(vbox)
468 self.window.show_all()
471 self.context_id = self.status_bar.get_context_id("statusbar")
472 self.update_status_bar()
474 def update_status_bar_thread():
476 gobject.idle_add( self.update_status_bar )
479 def update_wallet_thread():
482 self.wallet.new_session()
483 self.is_connected = True
484 self.info.set_text( self.wallet.message)
486 self.is_connected = False
487 traceback.print_exc(file=sys.stdout)
488 time.sleep(self.period)
493 if time.time() - get_servers_time > 5*60:
495 get_servers_time = time.time()
497 self.period = 15 if self.wallet.use_http() else 5
499 u = self.wallet.update()
500 self.is_connected = True
501 except BaseException:
502 print "starting new session"
504 except socket.gaierror:
505 self.is_connected = False
508 self.is_connected = False
510 traceback.print_exc(file=sys.stdout)
512 self.error = '' if self.is_connected else "Not connected"
515 gobject.idle_add( self.update_history_tab )
516 time.sleep(self.period)
518 thread.start_new_thread(update_wallet_thread, ())
519 thread.start_new_thread(update_status_bar_thread, ())
520 self.notebook.set_current_page(0)
523 def add_tab(self, page, name):
524 tab_label = gtk.Label(name)
526 self.notebook.append_page(page, tab_label)
529 def create_send_tab(self):
531 page = vbox = gtk.VBox()
535 payto_label = gtk.Label('Pay to:')
536 payto_label.set_size_request(100,10)
538 payto.pack_start(payto_label, False)
539 payto_entry = gtk.Entry()
540 payto_entry.set_size_request(350, 26)
542 payto.pack_start(payto_entry, False)
543 vbox.pack_start(payto, False, False, 5)
546 label_label = gtk.Label('Label:')
547 label_label.set_size_request(100,10)
549 label.pack_start(label_label, False)
550 label_entry = gtk.Entry()
551 label_entry.set_size_request(350, 26)
553 label.pack_start(label_entry, False)
554 vbox.pack_start(label, False, False, 5)
556 amount_box = gtk.HBox()
557 amount_label = gtk.Label('Amount:')
558 amount_label.set_size_request(100,-1)
560 amount_box.pack_start(amount_label, False)
561 amount_entry = gtk.Entry()
562 amount_entry.set_size_request(120, -1)
564 amount_box.pack_start(amount_entry, False)
565 vbox.pack_start(amount_box, False, False, 5)
567 send_button = gtk.Button("Send")
569 amount_box.pack_start(send_button, False, False, 5)
571 self.fee_box = fee_box = gtk.HBox()
572 fee_label = gtk.Label('Fee:')
573 fee_label.set_size_request(100,10)
574 fee_box.pack_start(fee_label, False)
575 fee_entry = gtk.Entry()
576 fee_entry.set_size_request(120, 26)
577 fee_entry.set_has_frame(False)
578 fee_entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
579 fee_box.pack_start(fee_entry, False)
581 send_button.connect("clicked", self.do_send, (payto_entry, label_entry, amount_entry, fee_entry))
582 vbox.pack_start(fee_box, False, False, 5)
584 self.user_fee = False
586 def entry_changed( entry, is_fee ):
587 amount = numbify(amount_entry)
588 fee = numbify(fee_entry)
589 if not is_fee: fee = None
591 self.fee_box.hide(); return
592 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
594 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
597 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
598 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
599 send_button.set_sensitive(True)
602 send_button.set_sensitive(False)
603 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
604 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
605 self.error = 'Not enough funds'
607 amount_entry.connect('changed', entry_changed, False)
608 fee_entry.connect('changed', entry_changed, True)
610 self.payto_entry = payto_entry
611 self.payto_amount_entry = amount_entry
612 self.payto_label_entry = label_entry
613 self.add_tab(page, 'Send')
615 def create_about_tab(self):
618 #self.info = gtk.Label('')
619 #self.info.set_selectable(True)
620 #page.pack_start(self.info)
622 tv.set_editable(False)
623 tv.set_cursor_visible(False)
625 self.info = tv.get_buffer()
626 self.add_tab(page, 'Wall')
628 def do_send(self, w, data):
629 payto_entry, label_entry, amount_entry, fee_entry = data
631 label = label_entry.get_text()
633 to_address = payto_entry.get_text()
634 if not self.wallet.is_valid(to_address):
635 self.show_message( "invalid bitcoin address")
639 amount = int( Decimal(amount_entry.get_text()) * 100000000 )
641 self.show_message( "invalid amount")
644 fee = int( Decimal(fee_entry.get_text()) * 100000000 )
646 self.show_message( "invalid fee")
649 password = password_dialog() if self.wallet.use_encryption else None
651 status, tx = self.wallet.mktx( to_address, amount, label, password, fee )
652 self.wallet.new_session() # we created a new change address
654 self.show_message(tx)
657 status, msg = self.wallet.sendtx( tx )
659 self.show_message( "payment sent.\n" + msg )
660 payto_entry.set_text("")
661 label_entry.set_text("")
662 amount_entry.set_text("")
663 fee_entry.set_text("")
665 self.update_sending_tab()
667 self.show_message( msg )
670 def treeview_key_press(self, treeview, event):
671 c = treeview.get_cursor()[0]
672 if event.keyval == gtk.keysyms.Up:
674 treeview.parent.grab_focus()
675 treeview.set_cursor((0,))
676 elif event.keyval == gtk.keysyms.Return and treeview == self.history_treeview:
677 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
678 self.show_message(tx_details)
681 def create_history_tab(self):
683 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
684 treeview = gtk.TreeView(model=self.history_list)
685 self.history_treeview = treeview
686 treeview.set_tooltip_column(7)
688 treeview.connect('key-press-event', self.treeview_key_press)
690 tvcolumn = gtk.TreeViewColumn('')
691 treeview.append_column(tvcolumn)
692 cell = gtk.CellRendererPixbuf()
693 tvcolumn.pack_start(cell, False)
694 tvcolumn.set_attributes(cell, stock_id=1)
696 tvcolumn = gtk.TreeViewColumn('Date')
697 treeview.append_column(tvcolumn)
698 cell = gtk.CellRendererText()
699 tvcolumn.pack_start(cell, False)
700 tvcolumn.add_attribute(cell, 'text', 2)
702 tvcolumn = gtk.TreeViewColumn('Label')
703 treeview.append_column(tvcolumn)
704 cell = gtk.CellRendererText()
705 cell.set_property('foreground', 'grey')
706 cell.set_property('family', 'monospace')
707 cell.set_property('editable', True)
708 def edited_cb(cell, path, new_text, h_list):
709 tx = h_list.get_value( h_list.get_iter(path), 0)
710 self.wallet.labels[tx] = new_text
712 self.update_history_tab()
713 cell.connect('edited', edited_cb, self.history_list)
714 def editing_started(cell, entry, path, h_list):
715 tx = h_list.get_value( h_list.get_iter(path), 0)
716 if not self.wallet.labels.get(tx): entry.set_text('')
717 cell.connect('editing-started', editing_started, self.history_list)
718 tvcolumn.set_expand(True)
719 tvcolumn.pack_start(cell, True)
720 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
722 tvcolumn = gtk.TreeViewColumn('Amount')
723 treeview.append_column(tvcolumn)
724 cell = gtk.CellRendererText()
725 cell.set_alignment(1, 0.5)
726 tvcolumn.pack_start(cell, False)
727 tvcolumn.add_attribute(cell, 'text', 5)
729 tvcolumn = gtk.TreeViewColumn('Balance')
730 treeview.append_column(tvcolumn)
731 cell = gtk.CellRendererText()
732 cell.set_alignment(1, 0.5)
733 tvcolumn.pack_start(cell, False)
734 tvcolumn.add_attribute(cell, 'text', 6)
736 tvcolumn = gtk.TreeViewColumn('Tooltip')
737 treeview.append_column(tvcolumn)
738 cell = gtk.CellRendererText()
739 tvcolumn.pack_start(cell, False)
740 tvcolumn.add_attribute(cell, 'text', 7)
741 tvcolumn.set_visible(False)
743 scroll = gtk.ScrolledWindow()
744 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
747 self.add_tab(scroll, 'History')
748 self.update_history_tab()
751 def create_recv_tab(self):
752 self.recv_list = gtk.ListStore(str, str, str)
753 self.add_tab( self.make_address_list(True), 'Receive')
754 self.update_receiving_tab()
756 def create_book_tab(self):
757 self.addressbook_list = gtk.ListStore(str, str, str)
758 self.add_tab( self.make_address_list(False), 'Contacts')
759 self.update_sending_tab()
761 def make_address_list(self, is_recv):
762 liststore = self.recv_list if is_recv else self.addressbook_list
763 treeview = gtk.TreeView(model= liststore)
764 treeview.connect('key-press-event', self.treeview_key_press)
767 tvcolumn = gtk.TreeViewColumn('Address')
768 treeview.append_column(tvcolumn)
769 cell = gtk.CellRendererText()
770 cell.set_property('family', 'monospace')
771 tvcolumn.pack_start(cell, True)
772 tvcolumn.add_attribute(cell, 'text', 0)
774 tvcolumn = gtk.TreeViewColumn('Label')
775 tvcolumn.set_expand(True)
776 treeview.append_column(tvcolumn)
777 cell = gtk.CellRendererText()
778 cell.set_property('editable', True)
779 def edited_cb2(cell, path, new_text, liststore):
780 address = liststore.get_value( liststore.get_iter(path), 0)
781 self.wallet.labels[address] = new_text
783 self.wallet.update_tx_labels()
784 self.update_receiving_tab()
785 self.update_sending_tab()
786 self.update_history_tab()
787 cell.connect('edited', edited_cb2, liststore)
788 tvcolumn.pack_start(cell, True)
789 tvcolumn.add_attribute(cell, 'text', 1)
791 tvcolumn = gtk.TreeViewColumn('Tx')
792 treeview.append_column(tvcolumn)
793 cell = gtk.CellRendererText()
794 tvcolumn.pack_start(cell, True)
795 tvcolumn.add_attribute(cell, 'text', 2)
797 scroll = gtk.ScrolledWindow()
798 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
802 button = gtk.Button("New address")
803 button.connect("clicked", self.newaddress_dialog, is_recv)
805 hbox.pack_start(button,False)
807 def showqrcode(w, treeview, liststore):
808 path, col = treeview.get_cursor()
810 address = liststore.get_value(liststore.get_iter(path), 0)
811 qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
815 size = qr.getModuleCount()*boxsize
816 def area_expose_cb(area, event):
817 style = area.get_style()
818 k = qr.getModuleCount()
821 gc = style.black_gc if qr.isDark(r, c) else style.white_gc
822 area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
823 area = gtk.DrawingArea()
824 area.set_size_request(size, size)
825 area.connect("expose-event", area_expose_cb)
827 dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
828 dialog.vbox.add(area)
832 button = gtk.Button("QR")
833 button.connect("clicked", showqrcode, treeview, liststore)
835 hbox.pack_start(button,False)
837 button = gtk.Button("Copy to clipboard")
838 def copy2clipboard(w, treeview, liststore):
840 path, col = treeview.get_cursor()
842 address = liststore.get_value( liststore.get_iter(path), 0)
843 if platform.system() == 'Windows':
844 from Tkinter import Tk
848 r.clipboard_append( address )
851 c = gtk.clipboard_get()
852 c.set_text( address )
853 button.connect("clicked", copy2clipboard, treeview, liststore)
855 hbox.pack_start(button,False)
858 button = gtk.Button("Pay to")
859 def payto(w, treeview, liststore):
860 path, col = treeview.get_cursor()
862 address = liststore.get_value( liststore.get_iter(path), 0)
863 self.payto_entry.set_text( address )
864 self.notebook.set_current_page(1)
865 self.payto_amount_entry.grab_focus()
867 button.connect("clicked", payto, treeview, liststore)
869 hbox.pack_start(button,False)
872 vbox.pack_start(scroll,True)
873 vbox.pack_start(hbox, False)
876 def update_status_bar(self):
877 c, u = self.wallet.get_balance()
878 if self.is_connected:
879 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
880 self.network_button.set_tooltip_text("Connected to %s.\n%d blocks\nresponse time: %f"%(self.wallet.host, self.wallet.blocks, self.wallet.rtime))
882 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
883 self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(self.wallet.host, self.wallet.blocks))
884 text = "Balance: %s "%( format_satoshis(c) )
885 if u: text += "[+ %s unconfirmed]"%( format_satoshis(u) )
886 if self.error: text = self.error
887 self.status_bar.pop(self.context_id)
888 self.status_bar.push(self.context_id, text)
890 def update_receiving_tab(self):
891 self.recv_list.clear()
892 for address in self.wallet.addresses:
893 if self.wallet.is_change(address):continue
894 label = self.wallet.labels.get(address)
896 h = self.wallet.history.get(address)
899 if not item['is_in'] : n=n+1
900 tx = "None" if n==0 else "%d"%n
901 self.recv_list.prepend((address, label, tx ))
903 def update_sending_tab(self):
904 # detect addresses that are not mine in history, add them here...
905 self.addressbook_list.clear()
906 for address in self.wallet.addressbook:
907 label = self.wallet.labels.get(address)
909 for item in self.wallet.tx_history.values():
910 if address in item['outputs'] : n=n+1
911 tx = "None" if n==0 else "%d"%n
912 self.addressbook_list.append((address, label, tx))
914 def update_history_tab(self):
915 cursor = self.history_treeview.get_cursor()[0]
916 self.history_list.clear()
918 for tx in self.wallet.get_tx_history():
919 tx_hash = tx['tx_hash']
921 conf = self.wallet.blocks - tx['height'] + 1
922 time_str = datetime.datetime.fromtimestamp( tx['nTime']).isoformat(' ')[:-3]
923 conf_icon = gtk.STOCK_APPLY
927 conf_icon = gtk.STOCK_EXECUTE
930 label = self.wallet.labels.get(tx_hash)
931 is_default_label = (label == '') or (label is None)
932 if is_default_label: label = tx['default_label']
933 tooltip = tx_hash + "\n%d confirmations"%conf
935 tx = self.wallet.tx_history.get(tx_hash)
936 details = "Transaction Details:\n\n"
937 details+= "Transaction ID:\n" + tx_hash + "\n\n"
938 details+= "Status: %d confirmations\n\n"%conf
939 details+= "Date: %s\n\n"%time_str
940 details+= "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n"
941 details+= "Outputs:\n-"+ '\n-'.join(tx['outputs'])
943 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
944 ('+' if v>0 else '') + format_satoshis(v), format_satoshis(balance), tooltip, details] )
945 if cursor: self.history_treeview.set_cursor( cursor )
949 def newaddress_dialog(self, w, is_recv):
953 title = "New sending address"
954 dialog = gtk.Dialog(title, parent=self.window,
955 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
956 buttons= ("cancel", 0, "ok",1) )
960 label_label = gtk.Label('Label:')
961 label_label.set_size_request(120,10)
963 label.pack_start(label_label)
964 label_entry = gtk.Entry()
966 label.pack_start(label_entry)
968 dialog.vbox.pack_start(label, False, True, 5)
971 address_label = gtk.Label('Address:')
972 address_label.set_size_request(120,10)
974 address.pack_start(address_label)
975 address_entry = gtk.Entry()
977 address.pack_start(address_entry)
979 dialog.vbox.pack_start(address, False, True, 5)
981 result = dialog.run()
982 address = address_entry.get_text()
983 label = label_entry.get_text()
987 if self.wallet.is_valid(address):
988 self.wallet.addressbook.append(address)
989 if label: self.wallet.labels[address] = label
991 self.update_sending_tab()
993 errorDialog = gtk.MessageDialog(
995 flags=gtk.DIALOG_MODAL,
996 buttons= gtk.BUTTONS_CLOSE,
997 message_format = "Invalid address")
1000 errorDialog.destroy()
1002 password = password_dialog() if self.wallet.use_encryption else None
1003 success, ret = self.wallet.get_new_address(password)
1004 self.wallet.new_session() # we created a new address
1007 #if label: self.wallet.labels[address] = label
1009 self.update_receiving_tab()
1012 errorDialog = gtk.MessageDialog(
1014 flags=gtk.DIALOG_MODAL,
1015 buttons= gtk.BUTTONS_CLOSE,
1016 message_format = msg)
1019 errorDialog.destroy()
1021 def network_dialog( self, w ):
1022 wallet = self.wallet
1024 image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
1025 if self.is_connected:
1026 status = "Connected to %s.\n%d blocks\nresponse time: %f"%(wallet.host, wallet.blocks, wallet.rtime)
1028 status = "Not connected"
1030 dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
1031 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
1032 dialog.set_title("Server")
1033 dialog.set_image(image)
1038 host_label = gtk.Label('Connect to:')
1039 host_label.set_size_request(100,-1)
1041 host.pack_start(host_label, False, False, 10)
1042 host_entry = gtk.Entry()
1043 host_entry.set_size_request(200,-1)
1044 host_entry.set_text(wallet.host+":%d"%wallet.port)
1046 host.pack_start(host_entry, False, False, 10)
1047 add_help_button(host, '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)')
1050 server_list = gtk.ListStore(str)
1051 for item in wallet.servers:
1052 server_list.append([item])
1054 treeview = gtk.TreeView(model=server_list)
1057 tvcolumn = gtk.TreeViewColumn('Active servers')
1058 treeview.append_column(tvcolumn)
1059 cell = gtk.CellRendererText()
1060 tvcolumn.pack_start(cell, False)
1061 tvcolumn.add_attribute(cell, 'text', 0)
1063 scroll = gtk.ScrolledWindow()
1064 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1065 scroll.add(treeview)
1068 vbox.pack_start(host, False,False, 5)
1069 vbox.pack_start(scroll)
1071 def my_treeview_cb(treeview):
1072 path, view_column = treeview.get_cursor()
1073 host = server_list.get_value( server_list.get_iter(path), 0)
1074 host_entry.set_text(host+":50000")
1075 treeview.connect('cursor-changed', my_treeview_cb)
1079 hh = host_entry.get_text()
1081 if r==gtk.RESPONSE_CANCEL:
1086 host, port = hh.split(':')
1092 self.show_message("error")
1095 if host!= wallet.host or port!=wallet.port:
1099 self.is_connected = False