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
25 gtk.gdk.threads_init()
28 def format_satoshis(x):
29 xx = ("%f"%(x*1e-8)).rstrip('0')
30 if xx[-1] =='.': xx+="00"
31 if xx[-2] =='.': xx+="0"
34 def numbify(entry, is_int = False):
35 text = entry.get_text().strip()
36 s = ''.join([i for i in text if i in '0123456789.'])
39 def init_wallet(wallet):
44 dialog = gtk.MessageDialog(
46 flags = gtk.DIALOG_MODAL,
47 buttons = gtk.BUTTONS_OK_CANCEL,
48 message_format = "Wallet not found. Please enter a passphrase to create or recover your wallet. Minimum length: 20 characters" )
51 p_label = gtk.Label('Passphrase:')
53 p_box.pack_start(p_label)
56 p_box.pack_start(p_entry)
58 dialog.vbox.pack_start(p_box, False, True, 0)
62 passphrase = p_entry.get_text()
65 if len(passphrase) < 20:
69 # disable password during recovery
70 # change_password_dialog(None, wallet)
72 wallet.passphrase = passphrase
74 run_settings_dialog( None, wallet, True)
76 dialog = gtk.MessageDialog(
78 flags = gtk.DIALOG_MODAL,
79 buttons = gtk.BUTTONS_CANCEL,
80 message_format = "Please wait..." )
83 def recover_thread( wallet, dialog, password ):
84 wallet.recover( password )
86 gobject.idle_add( dialog.destroy )
88 thread.start_new_thread( recover_thread, ( wallet, dialog, None ) ) # no password
93 def settings_dialog(wallet, is_recover):
95 dialog = gtk.MessageDialog(
97 flags = gtk.DIALOG_MODAL,
98 buttons = gtk.BUTTONS_OK_CANCEL,
99 message_format = "Please indicate the server, and the gap limit if you are recovering a lost wallet." if is_recover else '' )
102 dialog.get_image().hide()
103 dialog.set_title("settings")
107 pw_label = gtk.Label('Encryption: ')
108 pw_label.set_size_request(100,10)
110 pw.pack_start(pw_label,False, False, 10)
111 pw_button = gtk.Button( ('Yes' if wallet.use_encryption else 'No'))
112 pw_button.connect("clicked", change_password_dialog, wallet)
114 pw.pack_start(pw_button,False, False, 10)
118 gap_label = gtk.Label('Max. gap:')
119 gap_label.set_size_request(100,10)
121 gap.pack_start(gap_label,False, False, 10)
122 gap_entry = gtk.Entry()
123 gap_entry.set_text("%d"%wallet.gap_limit)
124 gap_entry.connect('changed', numbify, True)
126 gap.pack_start(gap_entry,False,False, 10)
127 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 passphrase.')
131 host_label = gtk.Label('Server:')
132 host_label.set_size_request(100,10)
134 host.pack_start(host_label,False, False, 10)
135 host_entry = gtk.Entry()
136 host_entry.set_text(wallet.host+":%d"%wallet.port)
138 host.pack_start(host_entry,False,False, 10)
139 add_help_button(host, 'The name and port number of your Bitcoin server, separated by a colon. Example: ecdsa.org:50000')
143 fee_entry = gtk.Entry()
145 fee_label = gtk.Label('Tx. fee:')
146 fee_label.set_size_request(100,10)
148 fee.pack_start(fee_label,False, False, 10)
149 fee_entry.set_text("%f"%(wallet.fee))
150 fee_entry.connect('changed', numbify, False)
152 fee.pack_start(fee_entry,False,False, 10)
153 add_help_button(fee, 'Transaction fee. Recommended value:0.005')
157 vbox.pack_start(pw, False, False, 5)
158 vbox.pack_start(host, False,False, 5)
159 vbox.pack_start(gap, False,False, 5)
160 vbox.pack_start(fee, False, False, 5)
161 return dialog, gap_entry, host_entry, fee_entry
164 def run_settings_dialog( widget, wallet, is_recovery):
165 dialog, gap_entry, host_entry, fee_entry = settings_dialog(wallet, is_recovery)
168 gap = gap_entry.get_text()
169 hh = host_entry.get_text()
170 fee = fee_entry.get_text()
179 wallet.gap_limit = int(gap)
182 wallet.fee = float(fee)
187 def show_message(message):
188 dialog = gtk.MessageDialog(
190 flags = gtk.DIALOG_MODAL,
191 buttons = gtk.BUTTONS_CLOSE,
192 message_format = message )
197 def password_line(label):
198 password = gtk.HBox()
199 password_label = gtk.Label(label)
200 password_label.set_size_request(120,10)
201 password_label.show()
202 password.pack_start(password_label,False, False, 10)
203 password_entry = gtk.Entry()
204 password_entry.set_visibility(False)
205 password_entry.show()
206 password.pack_start(password_entry,False,False, 10)
208 return password, password_entry
210 def password_dialog():
211 dialog = gtk.MessageDialog( None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
212 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Your wallet is encrypted.")
213 dialog.get_image().set_visible(False)
214 current_pw, current_pw_entry = password_line('Password:')
215 current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
216 dialog.vbox.pack_start(current_pw, False, True, 0)
218 result = dialog.run()
219 pw = current_pw_entry.get_text()
223 def change_password_dialog(button, wallet):
224 dialog = gtk.MessageDialog( None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
225 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, 'Change password')
226 if wallet.use_encryption:
227 current_pw, current_pw_entry = password_line('Old password:')
228 dialog.vbox.pack_start(current_pw, False, True, 0)
230 password, password_entry = password_line('New password:')
231 dialog.vbox.pack_start(password, False, True, 5)
232 password2, password2_entry = password_line('Confirm password:')
233 dialog.vbox.pack_start(password2, False, True, 5)
236 result = dialog.run()
237 password = current_pw_entry.get_text() if wallet.use_encryption else None
238 new_password = password_entry.get_text()
239 new_password2 = password2_entry.get_text()
245 passphrase = wallet.pw_decode( wallet.passphrase, password)
246 private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
248 show_message("sorry")
251 if new_password != new_password2:
252 show_message("passwords do not match")
255 wallet.use_encryption = (new_password != '')
256 wallet.passphrase = wallet.pw_encode( passphrase, new_password)
257 wallet.private_keys = wallet.pw_encode( repr( private_keys ), new_password)
260 button.set_label('Yes' if wallet.use_encryption else 'No')
263 def add_help_button(hbox, message):
264 button = gtk.Button('?')
265 button.connect("clicked", lambda x: show_message(message))
267 hbox.pack_start(button,False, False)
270 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
272 gobject.type_register(MyWindow)
273 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
274 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
279 def __init__(self, wallet):
284 self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
285 self.window.set_title(APP_NAME)
286 self.window.connect("destroy", gtk.main_quit)
287 self.window.set_border_width(0)
288 self.window.connect('mykeypress', gtk.main_quit)
289 self.window.set_default_size(650, 350)
293 self.notebook = gtk.Notebook()
294 self.create_history_tab()
295 self.create_send_tab()
296 self.create_recv_tab()
297 self.create_book_tab()
299 #self.add_tab( make_settings_box( self.wallet, False), 'Preferences')
300 self.create_about_tab()
303 vbox.pack_start(self.notebook, True, True, 2)
305 # status bar for balance, connection, blocks
306 self.status_bar = gtk.Statusbar()
307 vbox.pack_start(self.status_bar, False, False, 0)
309 self.status_image = gtk.Image()
310 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
311 self.status_image.set_alignment(True, 0.5 )
312 self.status_image.show()
313 self.status_bar.pack_end(self.status_image,False,False)
315 settings_icon = gtk.Image()
316 settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
317 settings_icon.set_alignment(True, False)
318 settings_icon.set_size_request(30,9 )
321 prefs_button = gtk.Button()
322 prefs_button.connect("clicked", run_settings_dialog, self.wallet, False)
323 prefs_button.add(settings_icon)
324 prefs_button.set_tooltip_text("Settings")
326 self.status_bar.pack_end(prefs_button,False,False)
328 self.window.add(vbox)
329 self.window.show_all()
331 self.context_id = self.status_bar.get_context_id("statusbar")
332 self.update_status_bar()
334 def update_status_bar_thread():
336 gobject.idle_add( self.update_status_bar )
339 def update_wallet_thread():
340 import socket, traceback, sys
343 self.wallet.new_session()
345 self.error = "Not connected"
348 self.info.set_text( self.wallet.message)
352 u = self.wallet.update()
354 self.error = "Not connected"
356 traceback.print_exc(file=sys.stdout)
358 self.update_time = time.time()
362 gobject.idle_add( self.update_history_tab )
365 thread.start_new_thread(update_wallet_thread, ())
366 thread.start_new_thread(update_status_bar_thread, ())
367 self.notebook.set_current_page(0)
370 def add_tab(self, page, name):
371 tab_label = gtk.Label(name)
373 self.notebook.append_page(page, tab_label)
376 def create_send_tab(self):
378 page = vbox = gtk.VBox()
382 payto_label = gtk.Label('Pay to:')
383 payto_label.set_size_request(100,10)
385 payto.pack_start(payto_label, False)
386 payto_entry = gtk.Entry()
387 payto_entry.set_size_request(350, 26)
389 payto.pack_start(payto_entry, False)
390 vbox.pack_start(payto, False, False, 5)
393 label_label = gtk.Label('Label:')
394 label_label.set_size_request(100,10)
396 label.pack_start(label_label, False)
397 label_entry = gtk.Entry()
398 label_entry.set_size_request(350, 26)
400 label.pack_start(label_entry, False)
401 vbox.pack_start(label, False, False, 5)
404 amount_label = gtk.Label('Amount:')
405 amount_label.set_size_request(100,10)
407 amount.pack_start(amount_label, False)
408 amount_entry = gtk.Entry()
409 amount_entry.set_size_request(100, 26)
410 amount_entry.connect('changed', numbify)
412 amount.pack_start(amount_entry, False)
413 vbox.pack_start(amount, False, False, 5)
415 button = gtk.Button("Send")
416 button.connect("clicked", self.do_send, (payto_entry, label_entry, amount_entry))
418 amount.pack_start(button, False, False, 5)
420 self.payto_entry = payto_entry
421 self.payto_amount_entry = amount_entry
422 self.payto_label_entry = label_entry
423 self.add_tab(page, 'Send')
425 def create_about_tab(self):
428 self.info = gtk.Label('')
429 self.info.set_selectable(True)
430 page.pack_start(self.info)
432 #tv.set_editable(False)
433 #tv.set_cursor_visible(False)
435 #self.info = tv.get_buffer()
436 self.add_tab(page, 'Board')
438 def do_send(self, w, data):
439 payto_entry, label_entry, amount_entry = data
441 label = label_entry.get_text()
443 to_address = payto_entry.get_text()
444 if not self.wallet.is_valid(to_address):
445 show_message( "invalid bitcoin address" )
449 amount = float(amount_entry.get_text())
451 show_message( "invalid amount" )
454 password = password_dialog() if self.wallet.use_encryption else None
456 status, msg = self.wallet.send( to_address, amount, label, password )
458 show_message( "payment sent.\n" + msg )
459 payto_entry.set_text("")
460 label_entry.set_text("")
461 amount_entry.set_text("")
466 def treeview_key_press(self, treeview, event):
467 c = treeview.get_cursor()[0]
468 if event.keyval == gtk.keysyms.Up:
470 treeview.parent.grab_focus()
471 treeview.set_cursor((0,))
472 elif event.keyval == gtk.keysyms.Return and treeview == self.history_treeview:
473 tx_hash = self.history_list.get_value( self.history_list.get_iter(c), 0)
474 tx = self.wallet.tx_history.get(tx_hash)
475 # print "tx details:\n"+repr(tx)
476 inputs = '\n-'.join(tx['inputs'])
477 outputs = '\n-'.join(tx['outputs'])
478 msg = tx_hash + "\n\ninputs:\n-"+ inputs + "\noutputs:\n-"+ outputs + "\n"
482 def create_history_tab(self):
484 self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str,str)
485 treeview = gtk.TreeView(model=self.history_list)
486 self.history_treeview = treeview
487 treeview.set_tooltip_column(7)
489 treeview.connect('key-press-event', self.treeview_key_press)
491 tvcolumn = gtk.TreeViewColumn('tx_id')
492 treeview.append_column(tvcolumn)
493 cell = gtk.CellRendererText()
494 tvcolumn.pack_start(cell, False)
495 tvcolumn.add_attribute(cell, 'text', 0)
496 tvcolumn.set_visible(False)
498 tvcolumn = gtk.TreeViewColumn('')
499 treeview.append_column(tvcolumn)
500 cell = gtk.CellRendererPixbuf()
501 tvcolumn.pack_start(cell, False)
502 tvcolumn.set_attributes(cell, stock_id=1)
504 tvcolumn = gtk.TreeViewColumn('Date')
505 treeview.append_column(tvcolumn)
506 cell = gtk.CellRendererText()
507 tvcolumn.pack_start(cell, False)
508 tvcolumn.add_attribute(cell, 'text', 2)
510 tvcolumn = gtk.TreeViewColumn('Label')
511 treeview.append_column(tvcolumn)
512 cell = gtk.CellRendererText()
513 cell.set_property('foreground', 'grey')
514 cell.set_property('family', 'monospace')
515 cell.set_property('editable', True)
516 def edited_cb(cell, path, new_text, h_list):
517 tx = h_list.get_value( h_list.get_iter(path), 0)
518 self.wallet.labels[tx] = new_text
520 self.update_history_tab()
521 cell.connect('edited', edited_cb, self.history_list)
522 def editing_started(cell, entry, path, h_list):
523 tx = h_list.get_value( h_list.get_iter(path), 0)
524 if not self.wallet.labels.get(tx): entry.set_text('')
525 cell.connect('editing-started', editing_started, self.history_list)
526 tvcolumn.set_expand(True)
527 tvcolumn.pack_start(cell, True)
528 tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
530 tvcolumn = gtk.TreeViewColumn('Amount')
531 treeview.append_column(tvcolumn)
532 cell = gtk.CellRendererText()
533 cell.set_alignment(1, 0.5)
534 tvcolumn.pack_start(cell, False)
535 tvcolumn.add_attribute(cell, 'text', 5)
537 tvcolumn = gtk.TreeViewColumn('Balance')
538 treeview.append_column(tvcolumn)
539 cell = gtk.CellRendererText()
540 cell.set_alignment(1, 0.5)
541 tvcolumn.pack_start(cell, False)
542 tvcolumn.add_attribute(cell, 'text', 6)
544 tvcolumn = gtk.TreeViewColumn('Tooltip')
545 treeview.append_column(tvcolumn)
546 cell = gtk.CellRendererText()
547 tvcolumn.pack_start(cell, False)
548 tvcolumn.add_attribute(cell, 'text', 7)
549 tvcolumn.set_visible(False)
551 scroll = gtk.ScrolledWindow()
552 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
555 self.add_tab(scroll, 'History')
556 self.update_history_tab()
559 def create_recv_tab(self):
560 self.recv_list = gtk.ListStore(str, str, str)
561 self.add_tab( self.make_address_list(True), 'Receive')
562 self.update_receiving_tab()
564 def create_book_tab(self):
565 self.addressbook_list = gtk.ListStore(str, str, str)
566 self.add_tab( self.make_address_list(False), 'Contacts')
567 self.update_sending_tab()
569 def make_address_list(self, is_recv):
570 liststore = self.recv_list if is_recv else self.addressbook_list
571 treeview = gtk.TreeView(model= liststore)
572 treeview.connect('key-press-event', self.treeview_key_press)
575 tvcolumn = gtk.TreeViewColumn('Address')
576 treeview.append_column(tvcolumn)
577 cell = gtk.CellRendererText()
578 cell.set_property('family', 'monospace')
579 tvcolumn.pack_start(cell, True)
580 tvcolumn.add_attribute(cell, 'text', 0)
582 tvcolumn = gtk.TreeViewColumn('Label')
583 tvcolumn.set_expand(True)
584 treeview.append_column(tvcolumn)
585 cell = gtk.CellRendererText()
586 cell.set_property('editable', True)
587 def edited_cb2(cell, path, new_text, liststore):
588 address = liststore.get_value( liststore.get_iter(path), 0)
589 self.wallet.labels[address] = new_text
591 self.wallet.update_tx_labels()
592 self.update_receiving_tab()
593 self.update_sending_tab()
594 self.update_history_tab()
595 cell.connect('edited', edited_cb2, liststore)
596 tvcolumn.pack_start(cell, True)
597 tvcolumn.add_attribute(cell, 'text', 1)
599 tvcolumn = gtk.TreeViewColumn('Tx')
600 treeview.append_column(tvcolumn)
601 cell = gtk.CellRendererText()
602 tvcolumn.pack_start(cell, True)
603 tvcolumn.add_attribute(cell, 'text', 2)
605 scroll = gtk.ScrolledWindow()
606 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
610 button = gtk.Button("New address")
611 button.connect("clicked", self.newaddress_dialog, is_recv)
613 hbox.pack_start(button,False)
615 button = gtk.Button("Copy to clipboard")
616 def copy2clipboard(w, treeview, liststore):
617 path, col = treeview.get_cursor()
619 address = liststore.get_value( liststore.get_iter(path), 0)
620 c = gtk.clipboard_get()
621 c.set_text( address )
622 button.connect("clicked", copy2clipboard, treeview, liststore)
624 hbox.pack_start(button,False)
627 button = gtk.Button("Pay to")
628 def payto(w, treeview, liststore):
629 path, col = treeview.get_cursor()
631 address = liststore.get_value( liststore.get_iter(path), 0)
632 self.payto_entry.set_text( address )
633 self.notebook.set_current_page(1)
634 self.payto_amount_entry.grab_focus()
636 button.connect("clicked", payto, treeview, liststore)
638 hbox.pack_start(button,False)
641 vbox.pack_start(scroll,True)
642 vbox.pack_start(hbox, False)
645 def update_status_bar(self):
646 c, u = self.wallet.get_balance()
647 dt = time.time() - self.update_time
649 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
650 self.status_image.set_tooltip_text("Connected to %s.\n%d blocks"%(self.wallet.host, self.wallet.blocks))
652 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
653 self.status_image.set_tooltip_text("Trying to contact %s.\n%d blocks"%(self.wallet.host, self.wallet.blocks))
654 text = "Balance: %s "%( format_satoshis(c) )
655 if u: text += "[+ %s unconfirmed]"%( format_satoshis(u) )
656 if self.error: text = self.error
657 self.status_bar.pop(self.context_id)
658 self.status_bar.push(self.context_id, text)
660 def update_receiving_tab(self):
661 self.recv_list.clear()
662 for address in self.wallet.addresses:
663 label = self.wallet.labels.get(address)
665 h = self.wallet.history.get(address)
668 if not item['is_in'] : n=n+1
669 tx = "None" if n==0 else "%d"%n
670 self.recv_list.prepend((address, label, tx ))
672 def update_sending_tab(self):
673 # detect addresses that are not mine in history, add them here...
674 self.addressbook_list.clear()
675 for address in self.wallet.addressbook:
676 label = self.wallet.labels.get(address)
678 for item in self.wallet.tx_history.values():
679 if address in item['outputs'] : n=n+1
680 tx = "None" if n==0 else "%d"%n
681 self.addressbook_list.append((address, label, tx))
683 def update_history_tab(self):
684 cursor = self.history_treeview.get_cursor()[0]
685 self.history_list.clear()
687 for tx in self.wallet.get_tx_history():
688 tx_hash = tx['tx_hash']
690 conf = self.wallet.blocks - tx['height'] + 1
691 time_str = datetime.datetime.fromtimestamp( tx['nTime']).isoformat(' ')[:-3]
692 conf_icon = gtk.STOCK_APPLY
696 conf_icon = gtk.STOCK_EXECUTE
699 label = self.wallet.labels.get(tx_hash)
700 is_default_label = (label == '') or (label is None)
701 if is_default_label: label = tx['default_label']
702 tooltip = tx_hash + "\n%d confirmations"%conf
703 self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
704 ('+' if v>0 else '') + format_satoshis(v), format_satoshis(balance), tooltip] )
705 if cursor: self.history_treeview.set_cursor( cursor )
709 def newaddress_dialog(self, w, is_recv):
713 title = "New sending address"
714 dialog = gtk.Dialog(title, parent=self.window,
715 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
716 buttons= ("cancel", 0, "ok",1) )
720 label_label = gtk.Label('Label:')
721 label_label.set_size_request(120,10)
723 label.pack_start(label_label)
724 label_entry = gtk.Entry()
726 label.pack_start(label_entry)
728 dialog.vbox.pack_start(label, False, True, 5)
731 address_label = gtk.Label('Address:')
732 address_label.set_size_request(120,10)
734 address.pack_start(address_label)
735 address_entry = gtk.Entry()
737 address.pack_start(address_entry)
739 dialog.vbox.pack_start(address, False, True, 5)
741 result = dialog.run()
742 address = address_entry.get_text()
743 label = label_entry.get_text()
747 if self.wallet.is_valid(address):
748 self.wallet.addressbook.append(address)
749 if label: self.wallet.labels[address] = label
751 self.update_sending_tab()
753 errorDialog = gtk.MessageDialog(
755 flags=gtk.DIALOG_MODAL,
756 buttons= gtk.BUTTONS_CLOSE,
757 message_format = "Invalid address")
760 errorDialog.destroy()
762 password = password_dialog() if self.wallet.use_encryption else None
763 success, ret = self.wallet.get_new_address(password)
766 #if label: self.wallet.labels[address] = label
768 self.update_receiving_tab()
771 errorDialog = gtk.MessageDialog(
773 flags=gtk.DIALOG_MODAL,
774 buttons= gtk.BUTTONS_CLOSE,
775 message_format = msg)
778 errorDialog.destroy()