address
[electrum-nvc.git] / client / gui.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2011 thomasv@gitorious
5 #
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.
10 #
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.
15 #
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/>.
18
19 import datetime
20 import thread, time, ast, sys
21 import socket, traceback
22 import pygtk
23 pygtk.require('2.0')
24 import gtk, gobject
25 import pyqrnative
26
27 gtk.gdk.threads_init()
28 APP_NAME = "Electrum"
29
30 def format_satoshis(x):
31     xx = ("%f"%(x*1e-8)).rstrip('0')
32     if xx[-1] =='.': xx+="00"
33     if xx[-2] =='.': xx+="0"
34     return xx
35
36 def numbify(entry, is_int = False):
37     text = entry.get_text().strip()
38     s = ''.join([i for i in text if i in '0123456789.'])
39     entry.set_text(s)
40
41
42 def show_seed_dialog(wallet, password):
43     import mnemonic
44     try:
45         seed = wallet.pw_decode( wallet.seed, password)
46         private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
47     except:
48         show_message("Incorrect password")
49         return
50     dialog = gtk.MessageDialog(
51         parent = None,
52         flags = gtk.DIALOG_MODAL, 
53         buttons = gtk.BUTTONS_OK, 
54         message_format = "Your wallet generation seed is:\n\n" + seed \
55             + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
56             + "Alternatively, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
57     dialog.show()
58     dialog.run()
59     dialog.destroy()
60
61 def init_wallet(wallet):
62
63     if not wallet.read():
64
65         # ask if the user wants to create a new wallet, or recover from a seed. 
66         # if he wants to recover, and nothing is found, do not create wallet
67         dialog = gtk.Dialog("electrum", parent=None, 
68                             flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, 
69                             buttons= ("create", 0, "restore",1, "cancel",2)  )
70
71         label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?"  )
72         label.show()
73         dialog.vbox.pack_start(label)
74         dialog.show()
75         r = dialog.run()
76         dialog.destroy()
77         if r==2:
78             sys.exit(1)
79         
80         is_recovery = (r==1)
81
82         if not is_recovery:
83
84             wallet.new_seed(None)
85
86             # ask for the server.
87             run_settings_dialog(wallet, is_create=True, is_recovery=False)
88
89             # generate first key
90             wallet.create_new_address(False, None)
91
92             # run a dialog indicating the seed, ask the user to remember it
93             show_seed_dialog(wallet, None)
94             
95             #ask for password
96             change_password_dialog(wallet, None)
97
98         else:
99             # ask for the server, seed and gap.
100             run_settings_dialog(wallet, is_create=True, is_recovery=True)
101
102             dialog = gtk.MessageDialog(
103                 parent = None,
104                 flags = gtk.DIALOG_MODAL, 
105                 buttons = gtk.BUTTONS_CANCEL, 
106                 message_format = "Please wait..."  )
107             dialog.show()
108
109             def recover_thread( wallet, dialog, password ):
110                 wallet.is_found = wallet.recover( password )
111                 if wallet.is_found:
112                     wallet.save()
113                 gobject.idle_add( dialog.destroy )
114
115             thread.start_new_thread( recover_thread, ( wallet, dialog, None ) ) # no password
116             r = dialog.run()
117             dialog.destroy()
118             if r==gtk.RESPONSE_CANCEL: sys.exit(1)
119             if not wallet.is_found:
120                 show_message("No transactions found for this seed")
121
122
123 def settings_dialog(wallet, is_create,  is_recovery):
124
125     if is_create:
126         dialog = gtk.MessageDialog(
127             parent = None,
128             flags = gtk.DIALOG_MODAL, 
129             buttons = gtk.BUTTONS_OK_CANCEL, 
130             message_format = "Please indicate the server and port number" if not is_recovery else 'Please enter your wallet seed or the corresponding mnemonic list of words, the server and the gap limit')
131     else:
132         dialog = gtk.MessageDialog( None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
133                                     gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL,  "Settings")
134         dialog.get_image().set_visible(False)
135
136     vbox = dialog.vbox
137     dialog.set_default_response(gtk.RESPONSE_OK)
138
139     if is_recovery:
140         # ask seed, server and gap in the same dialog
141         seed_box = gtk.HBox()
142         seed_label = gtk.Label('Seed or mnemonic:')
143         seed_label.set_size_request(150,-1)
144         seed_box.pack_start(seed_label, False, False, 10)
145         seed_label.show()
146         seed_entry = gtk.Entry()
147         seed_entry.show()
148         seed_entry.set_size_request(450,-1)
149         seed_box.pack_start(seed_entry, False, False, 10)
150         add_help_button(seed_box, '.')
151         seed_box.show()
152         vbox.pack_start(seed_box, False, False, 5)    
153
154     if is_recovery or (not is_create):
155         gap = gtk.HBox()
156         gap_label = gtk.Label('Gap limit:')
157         gap_label.set_size_request(150,10)
158         gap_label.show()
159         gap.pack_start(gap_label,False, False, 10)
160         gap_entry = gtk.Entry()
161         gap_entry.set_text("%d"%wallet.gap_limit)
162         gap_entry.connect('changed', numbify, True)
163         gap_entry.show()
164         gap.pack_start(gap_entry,False,False, 10)
165         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.')
166         gap.show()
167         vbox.pack_start(gap, False,False, 5)
168
169     host = gtk.HBox()
170     host_label = gtk.Label('Server:')
171     host_label.set_size_request(150,10)
172     host_label.show()
173     host.pack_start(host_label,False, False, 10)
174     host_entry = gtk.Entry()
175     host_entry.set_text(wallet.host+":%d"%wallet.port)
176     host_entry.show()
177     host.pack_start(host_entry,False,False, 10)
178     add_help_button(host, 'The name and port number of your Bitcoin server, separated by a colon. Example: "ecdsa.org:50000". If no port number is provided, the http port 80 will be tried.')
179     host.show()
180     vbox.pack_start(host, False,False, 5)
181
182     if not is_create:
183         fee = gtk.HBox()
184         fee_entry = gtk.Entry()
185         fee_label = gtk.Label('Tx. fee:')
186         fee_label.set_size_request(150,10)
187         fee_label.show()
188         fee.pack_start(fee_label,False, False, 10)
189         fee_entry.set_text("%f"%(wallet.fee))
190         fee_entry.connect('changed', numbify, False)
191         fee_entry.show()
192         fee.pack_start(fee_entry,False,False, 10)
193         add_help_button(fee, 'Transaction fee. Recommended value:0.005')
194         fee.show()
195         vbox.pack_start(fee, False,False, 5)
196             
197     if not is_create:
198         return dialog, fee_entry, gap_entry, host_entry
199     elif is_recovery:
200         return dialog, seed_entry, gap_entry, host_entry
201     else:
202         return dialog, host_entry
203
204
205 def run_settings_dialog( wallet, is_create, is_recovery):
206
207     if not is_create:
208         dialog, fee_entry, gap_entry, host_entry = settings_dialog(wallet, is_create, is_recovery)
209     elif is_recovery:
210         dialog, seed_entry, gap_entry, host_entry = settings_dialog(wallet, is_create, is_recovery)
211     else:
212         dialog, host_entry, = settings_dialog(wallet, is_create, is_recovery)
213
214     dialog.show()
215     r = dialog.run()
216     hh = host_entry.get_text()
217     if is_recovery:
218         gap = gap_entry.get_text()
219         seed = seed_entry.get_text()
220         try:
221             seed.decode('hex')
222         except:
223             import mnemonic
224             print "not hex, trying decode"
225             seed = mnemonic.mn_decode( seed.split(' ') )
226     dialog.destroy()
227     if r==gtk.RESPONSE_CANCEL:
228         if is_create: sys.exit(1)
229         else: return
230
231     try:
232         if ':' in hh:
233             host, port = hh.split(':')
234             port = int(port)
235         else:
236             host = hh
237             port = 80
238         if is_recovery: gap = int(gap)
239     except:
240         show_message("error")
241         return
242
243     wallet.host = host
244     wallet.port = port
245     if is_recovery:
246         wallet.seed = seed
247         wallet.gap_limit = gap
248     wallet.save()
249
250
251 def show_message(message):
252     dialog = gtk.MessageDialog(
253         parent = None,
254         flags = gtk.DIALOG_MODAL, 
255         buttons = gtk.BUTTONS_CLOSE, 
256         message_format = message )
257     dialog.show()
258     dialog.run()
259     dialog.destroy()
260
261 def password_line(label):
262     password = gtk.HBox()
263     password_label = gtk.Label(label)
264     password_label.set_size_request(120,10)
265     password_label.show()
266     password.pack_start(password_label,False, False, 10)
267     password_entry = gtk.Entry()
268     password_entry.set_size_request(300,-1)
269     password_entry.set_visibility(False)
270     password_entry.show()
271     password.pack_start(password_entry,False,False, 10)
272     password.show()
273     return password, password_entry
274
275 def password_dialog():
276     dialog = gtk.MessageDialog( None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
277                                 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL,  "Please enter your password.")
278     dialog.get_image().set_visible(False)
279     current_pw, current_pw_entry = password_line('Password:')
280     current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
281     dialog.vbox.pack_start(current_pw, False, True, 0)
282     dialog.show()
283     result = dialog.run()
284     pw = current_pw_entry.get_text()
285     dialog.destroy()
286     if result: return pw
287
288 def change_password_dialog(wallet, icon):
289     if icon:
290         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'
291     else:
292         msg = "Please choose a password to encrypt your wallet keys"
293
294     dialog = gtk.MessageDialog( None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
295         
296
297     if wallet.use_encryption:
298         current_pw, current_pw_entry = password_line('Current password:')
299         dialog.vbox.pack_start(current_pw, False, True, 0)
300
301     password, password_entry = password_line('New password:')
302     dialog.vbox.pack_start(password, False, True, 5)
303     password2, password2_entry = password_line('Confirm password:')
304     dialog.vbox.pack_start(password2, False, True, 5)
305
306     dialog.show()
307     result = dialog.run()
308     password = current_pw_entry.get_text() if wallet.use_encryption else None
309     new_password = password_entry.get_text()
310     new_password2 = password2_entry.get_text()
311     dialog.destroy()
312     if result == gtk.RESPONSE_CANCEL: 
313         return
314
315     try:
316         seed = wallet.pw_decode( wallet.seed, password)
317         private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
318     except:
319         show_message("Incorrect password")
320         return
321
322     if new_password != new_password2:
323         show_message("passwords do not match")
324         return
325
326     wallet.use_encryption = (new_password != '')
327     wallet.seed = wallet.pw_encode( seed, new_password)
328     wallet.private_keys = wallet.pw_encode( repr( private_keys ), new_password)
329     wallet.save()
330
331     if icon:
332         if wallet.use_encryption:
333             icon.set_tooltip_text('wallet is encrypted')
334         else:
335             icon.set_tooltip_text('wallet is unencrypted')
336
337
338 def add_help_button(hbox, message):
339     button = gtk.Button('?')
340     button.connect("clicked", lambda x: show_message(message))
341     button.show()
342     hbox.pack_start(button,False, False)
343
344
345 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
346
347 gobject.type_register(MyWindow)
348 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
349 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
350
351
352 class BitcoinGUI:
353
354     def __init__(self, wallet):
355         self.error = ''
356         self.wallet = wallet
357         self.period = 5
358
359         self.update_time = 0 
360         self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
361         self.window.set_title(APP_NAME)
362         self.window.connect("destroy", gtk.main_quit)
363         self.window.set_border_width(0)
364         self.window.connect('mykeypress', gtk.main_quit)
365         self.window.set_default_size(670, 350)
366
367         vbox = gtk.VBox()
368
369         self.notebook = gtk.Notebook()
370         self.create_history_tab()
371         self.create_send_tab()
372         self.create_recv_tab()
373         self.create_book_tab()
374         #self.add_tab( make_settings_box( self.wallet, False), 'Preferences')
375         self.create_about_tab()
376         self.notebook.show()
377         vbox.pack_start(self.notebook, True, True, 2)
378         
379         self.status_bar = gtk.Statusbar()
380         vbox.pack_start(self.status_bar, False, False, 0)
381
382         self.status_image = gtk.Image()
383         self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
384         self.status_image.set_alignment(True, 0.5  )
385         self.status_image.show()
386         self.status_bar.pack_end(self.status_image, False, False)
387
388
389         def seedb(w, wallet):
390             if wallet.use_encryption:
391                 password = password_dialog()
392                 if not password: return
393             else: password = None
394             show_seed_dialog(wallet, password)
395         button = gtk.Button('S')
396         button.connect("clicked", seedb, wallet )
397         button.set_relief(gtk.RELIEF_NONE)
398         button.show()
399         self.status_bar.pack_end(button,False, False)
400
401         settings_icon = gtk.Image()
402         settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
403         settings_icon.set_alignment(0.5, 0.5)
404         settings_icon.set_size_request(16,16 )
405         settings_icon.show()
406
407         prefs_button = gtk.Button()
408         prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, False, False) )
409         prefs_button.add(settings_icon)
410         prefs_button.set_tooltip_text("Settings")
411         prefs_button.set_relief(gtk.RELIEF_NONE)
412         prefs_button.show()
413         self.status_bar.pack_end(prefs_button,False,False)
414
415         pw_icon = gtk.Image()
416         pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
417         pw_icon.set_alignment(0.5, 0.5)
418         pw_icon.set_size_request(16,16 )
419         pw_icon.show()
420
421         password_button = gtk.Button()
422         password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, pw_icon))
423         password_button.add(pw_icon)
424         password_button.set_relief(gtk.RELIEF_NONE)
425         password_button.show()
426         self.status_bar.pack_end(password_button,False,False)
427
428         self.window.add(vbox)
429         self.window.show_all()
430
431         self.context_id = self.status_bar.get_context_id("statusbar")
432         self.update_status_bar()
433
434         def update_status_bar_thread():
435             while True:
436                 gobject.idle_add( self.update_status_bar )
437                 time.sleep(0.5)
438
439         def update_wallet_thread():
440             while True:
441                 try:
442                     self.wallet.new_session()
443                 except:
444                     self.error = "Not connected"
445                     time.sleep(self.period)
446                     continue
447
448                 self.info.set_text( self.wallet.message)
449
450                 while True:
451                     self.period = 15 if self.wallet.use_http() else 5
452                     try:
453                         u = self.wallet.update()
454                     except socket.gaierror:
455                         self.error = "Not connected"
456                         break
457                     except:
458                         self.error = "Not connected"
459                         print "error"
460                         traceback.print_exc(file=sys.stdout)
461                         break
462                     self.update_time = time.time()
463                     self.error = ''
464                     if u:
465                         self.wallet.save()
466                         gobject.idle_add( self.update_history_tab )
467                     time.sleep(self.period)
468                     
469         thread.start_new_thread(update_wallet_thread, ())
470         thread.start_new_thread(update_status_bar_thread, ())
471         self.notebook.set_current_page(0)
472
473
474     def add_tab(self, page, name):
475         tab_label = gtk.Label(name)
476         tab_label.show()
477         self.notebook.append_page(page, tab_label)
478
479
480     def create_send_tab(self):
481
482         page = vbox = gtk.VBox()
483         page.show()
484
485         payto = gtk.HBox()
486         payto_label = gtk.Label('Pay to:')
487         payto_label.set_size_request(100,10)
488         payto_label.show()
489         payto.pack_start(payto_label, False)
490         payto_entry = gtk.Entry()
491         payto_entry.set_size_request(350, 26)
492         payto_entry.show()
493         payto.pack_start(payto_entry, False)
494         vbox.pack_start(payto, False, False, 5)
495         
496         label = gtk.HBox()
497         label_label = gtk.Label('Label:')
498         label_label.set_size_request(100,10)
499         label_label.show()
500         label.pack_start(label_label, False)
501         label_entry = gtk.Entry()
502         label_entry.set_size_request(350, 26)
503         label_entry.show()
504         label.pack_start(label_entry, False)
505         vbox.pack_start(label, False, False, 5)
506
507         amount = gtk.HBox()
508         amount_label = gtk.Label('Amount:')
509         amount_label.set_size_request(100,10)
510         amount_label.show()
511         amount.pack_start(amount_label, False)
512         amount_entry = gtk.Entry()
513         amount_entry.set_size_request(100, 26) 
514         amount_entry.connect('changed', numbify)
515         amount_entry.show()
516         amount.pack_start(amount_entry, False)
517         vbox.pack_start(amount, False, False, 5)
518
519         button = gtk.Button("Send")
520         button.connect("clicked", self.do_send, (payto_entry, label_entry, amount_entry))
521         button.show()
522         amount.pack_start(button, False, False, 5)
523
524         self.payto_entry = payto_entry
525         self.payto_amount_entry = amount_entry
526         self.payto_label_entry = label_entry
527         self.add_tab(page, 'Send')
528
529     def create_about_tab(self):
530         page = gtk.VBox()
531         page.show()
532         self.info = gtk.Label('')  
533         self.info.set_selectable(True)
534         page.pack_start(self.info)
535         #tv = gtk.TextView()
536         #tv.set_editable(False)
537         #tv.set_cursor_visible(False)
538         #page.pack_start(tv)
539         #self.info = tv.get_buffer()
540         self.add_tab(page, 'Wall')
541
542     def do_send(self, w, data):
543         payto_entry, label_entry, amount_entry = data
544         
545         label = label_entry.get_text()
546
547         to_address = payto_entry.get_text()
548         if not self.wallet.is_valid(to_address):
549             show_message( "invalid bitcoin address" )
550             return
551
552         try:
553             amount = float(amount_entry.get_text())
554         except:
555             show_message( "invalid amount" )
556             return
557
558         password = password_dialog() if self.wallet.use_encryption else None
559
560         status, tx = self.wallet.mktx( to_address, amount, label, password )
561         self.wallet.new_session() # we created a new change address
562         if not status:
563             show_message(tx)
564             return
565
566         status, msg = self.wallet.sendtx( tx )
567         if status:
568             show_message( "payment sent.\n" + msg )
569             payto_entry.set_text("")
570             label_entry.set_text("")
571             amount_entry.set_text("")
572             self.update_sending_tab()
573         else:
574             show_message( msg )
575
576
577     def treeview_key_press(self, treeview, event):
578         c = treeview.get_cursor()[0]
579         if event.keyval == gtk.keysyms.Up:
580             if c and c[0] == 0:
581                 treeview.parent.grab_focus()
582                 treeview.set_cursor((0,))
583         elif event.keyval == gtk.keysyms.Return and treeview == self.history_treeview:
584             tx_hash = self.history_list.get_value( self.history_list.get_iter(c), 0)
585             tx = self.wallet.tx_history.get(tx_hash)
586             # print "tx details:\n"+repr(tx)
587             inputs = '\n-'.join(tx['inputs'])
588             outputs = '\n-'.join(tx['outputs'])
589             msg = tx_hash + "\n\ninputs:\n-"+ inputs + "\noutputs:\n-"+ outputs + "\n"
590             show_message(msg)
591         return False
592
593     def create_history_tab(self):
594
595         self.history_list = gtk.ListStore(str, str, str, str, 'gboolean',  str, str,str)
596         treeview = gtk.TreeView(model=self.history_list)
597         self.history_treeview = treeview
598         treeview.set_tooltip_column(7)
599         treeview.show()
600         treeview.connect('key-press-event', self.treeview_key_press)
601
602         tvcolumn = gtk.TreeViewColumn('tx_id')
603         treeview.append_column(tvcolumn)
604         cell = gtk.CellRendererText()
605         tvcolumn.pack_start(cell, False)
606         tvcolumn.add_attribute(cell, 'text', 0)
607         tvcolumn.set_visible(False)
608
609         tvcolumn = gtk.TreeViewColumn('')
610         treeview.append_column(tvcolumn)
611         cell = gtk.CellRendererPixbuf()
612         tvcolumn.pack_start(cell, False)
613         tvcolumn.set_attributes(cell, stock_id=1)
614
615         tvcolumn = gtk.TreeViewColumn('Date')
616         treeview.append_column(tvcolumn)
617         cell = gtk.CellRendererText()
618         tvcolumn.pack_start(cell, False)
619         tvcolumn.add_attribute(cell, 'text', 2)
620
621         tvcolumn = gtk.TreeViewColumn('Label')
622         treeview.append_column(tvcolumn)
623         cell = gtk.CellRendererText()
624         cell.set_property('foreground', 'grey')
625         cell.set_property('family', 'monospace')
626         cell.set_property('editable', True)
627         def edited_cb(cell, path, new_text, h_list):
628             tx = h_list.get_value( h_list.get_iter(path), 0)
629             self.wallet.labels[tx] = new_text
630             self.wallet.save() 
631             self.update_history_tab()
632         cell.connect('edited', edited_cb, self.history_list)
633         def editing_started(cell, entry, path, h_list):
634             tx = h_list.get_value( h_list.get_iter(path), 0)
635             if not self.wallet.labels.get(tx): entry.set_text('')
636         cell.connect('editing-started', editing_started, self.history_list)
637         tvcolumn.set_expand(True)
638         tvcolumn.pack_start(cell, True)
639         tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
640
641         tvcolumn = gtk.TreeViewColumn('Amount')
642         treeview.append_column(tvcolumn)
643         cell = gtk.CellRendererText()
644         cell.set_alignment(1, 0.5)
645         tvcolumn.pack_start(cell, False)
646         tvcolumn.add_attribute(cell, 'text', 5)
647
648         tvcolumn = gtk.TreeViewColumn('Balance')
649         treeview.append_column(tvcolumn)
650         cell = gtk.CellRendererText()
651         cell.set_alignment(1, 0.5)
652         tvcolumn.pack_start(cell, False)
653         tvcolumn.add_attribute(cell, 'text', 6)
654
655         tvcolumn = gtk.TreeViewColumn('Tooltip')
656         treeview.append_column(tvcolumn)
657         cell = gtk.CellRendererText()
658         tvcolumn.pack_start(cell, False)
659         tvcolumn.add_attribute(cell, 'text', 7)
660         tvcolumn.set_visible(False)
661
662         scroll = gtk.ScrolledWindow()
663         scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
664         scroll.add(treeview)
665
666         self.add_tab(scroll, 'History')
667         self.update_history_tab()
668
669
670     def create_recv_tab(self):
671         self.recv_list = gtk.ListStore(str, str, str)
672         self.add_tab( self.make_address_list(True), 'Receive')
673         self.update_receiving_tab()
674
675     def create_book_tab(self):
676         self.addressbook_list = gtk.ListStore(str, str, str)
677         self.add_tab( self.make_address_list(False), 'Contacts')
678         self.update_sending_tab()
679
680     def make_address_list(self, is_recv):
681         liststore = self.recv_list if is_recv else self.addressbook_list
682         treeview = gtk.TreeView(model= liststore)
683         treeview.connect('key-press-event', self.treeview_key_press)
684         treeview.show()
685
686         tvcolumn = gtk.TreeViewColumn('Address')
687         treeview.append_column(tvcolumn)
688         cell = gtk.CellRendererText()
689         cell.set_property('family', 'monospace')
690         tvcolumn.pack_start(cell, True)
691         tvcolumn.add_attribute(cell, 'text', 0)
692
693         tvcolumn = gtk.TreeViewColumn('Label')
694         tvcolumn.set_expand(True)
695         treeview.append_column(tvcolumn)
696         cell = gtk.CellRendererText()
697         cell.set_property('editable', True)
698         def edited_cb2(cell, path, new_text, liststore):
699             address = liststore.get_value( liststore.get_iter(path), 0)
700             self.wallet.labels[address] = new_text
701             self.wallet.save() 
702             self.wallet.update_tx_labels()
703             self.update_receiving_tab()
704             self.update_sending_tab()
705             self.update_history_tab()
706         cell.connect('edited', edited_cb2, liststore)
707         tvcolumn.pack_start(cell, True)
708         tvcolumn.add_attribute(cell, 'text', 1)
709
710         tvcolumn = gtk.TreeViewColumn('Tx')
711         treeview.append_column(tvcolumn)
712         cell = gtk.CellRendererText()
713         tvcolumn.pack_start(cell, True)
714         tvcolumn.add_attribute(cell, 'text', 2)
715
716         scroll = gtk.ScrolledWindow()
717         scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
718         scroll.add(treeview)
719
720         hbox = gtk.HBox()
721         button = gtk.Button("New address")
722         button.connect("clicked", self.newaddress_dialog, is_recv)
723         button.show()
724         hbox.pack_start(button,False)
725
726         def showqrcode(w, treeview, liststore):
727             path, col = treeview.get_cursor()
728             if not path: return
729             address = liststore.get_value(liststore.get_iter(path), 0)
730             qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
731             qr.addData(address)
732             qr.make()
733             boxsize = 7
734             size = qr.getModuleCount()*boxsize
735             def area_expose_cb(area, event):
736                 style = area.get_style()
737                 k = qr.getModuleCount()
738                 for r in range(k):
739                     for c in range(k):
740                         gc = style.black_gc if qr.isDark(r, c) else style.white_gc
741                         area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
742             area = gtk.DrawingArea()
743             area.set_size_request(size, size)
744             area.connect("expose-event", area_expose_cb)
745             area.show()
746             dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
747             dialog.vbox.add(area)
748             dialog.run()
749             dialog.destroy()
750
751         button = gtk.Button("QR")
752         button.connect("clicked", showqrcode, treeview, liststore)
753         button.show()
754         hbox.pack_start(button,False)
755
756         button = gtk.Button("Copy to clipboard")
757         def copy2clipboard(w, treeview, liststore):
758             import platform
759             path, col =  treeview.get_cursor()
760             if path:
761                 address =  liststore.get_value( liststore.get_iter(path), 0)
762                 if platform.system() == 'Windows':
763                     from Tkinter import Tk
764                     r = Tk()
765                     r.withdraw()
766                     r.clipboard_clear()
767                     r.clipboard_append( address )
768                     r.destroy()
769                 else:
770                     c = gtk.clipboard_get()
771                     c.set_text( address )
772         button.connect("clicked", copy2clipboard, treeview, liststore)
773         button.show()
774         hbox.pack_start(button,False)
775
776         if not is_recv:
777             button = gtk.Button("Pay to")
778             def payto(w, treeview, liststore):
779                 path, col =  treeview.get_cursor()
780                 if path:
781                     address =  liststore.get_value( liststore.get_iter(path), 0)
782                     self.payto_entry.set_text( address )
783                     self.notebook.set_current_page(1)
784                     self.payto_amount_entry.grab_focus()
785
786             button.connect("clicked", payto, treeview, liststore)
787             button.show()
788             hbox.pack_start(button,False)
789
790         vbox = gtk.VBox()
791         vbox.pack_start(scroll,True)
792         vbox.pack_start(hbox, False)
793         return vbox
794
795     def update_status_bar(self):
796         c, u = self.wallet.get_balance()
797         dt = time.time() - self.update_time
798         if dt < 2*self.period:
799             self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
800             self.status_image.set_tooltip_text("Connected to %s.\n%d blocks\nresponse time: %f"%(self.wallet.host, self.wallet.blocks, self.wallet.rtime))
801         else:
802             self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
803             self.status_image.set_tooltip_text("Trying to contact %s.\n%d blocks"%(self.wallet.host, self.wallet.blocks))
804         text =  "Balance: %s "%( format_satoshis(c) )
805         if u: text +=  "[+ %s unconfirmed]"%( format_satoshis(u) )
806         if self.error: text = self.error
807         self.status_bar.pop(self.context_id) 
808         self.status_bar.push(self.context_id, text) 
809
810     def update_receiving_tab(self):
811         self.recv_list.clear()
812         for address in self.wallet.addresses:
813             if self.wallet.is_change(address):continue
814             label = self.wallet.labels.get(address)
815             n = 0 
816             h = self.wallet.history.get(address)
817             if h:
818                 for item in h:
819                     if not item['is_in'] : n=n+1
820             tx = "None" if n==0 else "%d"%n
821             self.recv_list.prepend((address, label, tx ))
822
823     def update_sending_tab(self):
824         # detect addresses that are not mine in history, add them here...
825         self.addressbook_list.clear()
826         for address in self.wallet.addressbook:
827             label = self.wallet.labels.get(address)
828             n = 0 
829             for item in self.wallet.tx_history.values():
830                 if address in item['outputs'] : n=n+1
831             tx = "None" if n==0 else "%d"%n
832             self.addressbook_list.append((address, label, tx))
833
834     def update_history_tab(self):
835         cursor = self.history_treeview.get_cursor()[0]
836         self.history_list.clear()
837         balance = 0 
838         for tx in self.wallet.get_tx_history():
839             tx_hash = tx['tx_hash']
840             if tx['height']:
841                 conf = self.wallet.blocks - tx['height'] + 1
842                 time_str = datetime.datetime.fromtimestamp( tx['nTime']).isoformat(' ')[:-3]
843                 conf_icon = gtk.STOCK_APPLY
844             else:
845                 conf = 0
846                 time_str = 'pending'
847                 conf_icon = gtk.STOCK_EXECUTE
848             v = tx['value']
849             balance += v 
850             label = self.wallet.labels.get(tx_hash)
851             is_default_label = (label == '') or (label is None)
852             if is_default_label: label = tx['default_label']
853             tooltip = tx_hash + "\n%d confirmations"%conf 
854             self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, 
855                                         ('+' if v>0 else '') + format_satoshis(v), format_satoshis(balance), tooltip] )
856         if cursor: self.history_treeview.set_cursor( cursor )
857
858
859
860     def newaddress_dialog(self, w, is_recv):
861
862         if not is_recv:
863
864             title = "New sending address" 
865             dialog = gtk.Dialog(title, parent=self.window, 
866                                 flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, 
867                                 buttons= ("cancel", 0, "ok",1)  )
868             dialog.show()
869
870             label = gtk.HBox()
871             label_label = gtk.Label('Label:')
872             label_label.set_size_request(120,10)
873             label_label.show()
874             label.pack_start(label_label)
875             label_entry = gtk.Entry()
876             label_entry.show()
877             label.pack_start(label_entry)
878             label.show()
879             dialog.vbox.pack_start(label, False, True, 5)
880
881             address = gtk.HBox()
882             address_label = gtk.Label('Address:')
883             address_label.set_size_request(120,10)
884             address_label.show()
885             address.pack_start(address_label)
886             address_entry = gtk.Entry()
887             address_entry.show()
888             address.pack_start(address_entry)
889             address.show()
890             dialog.vbox.pack_start(address, False, True, 5)
891
892             result = dialog.run()
893             address = address_entry.get_text()
894             label = label_entry.get_text()
895             dialog.destroy()
896
897             if result == 1:
898                 if self.wallet.is_valid(address):
899                     self.wallet.addressbook.append(address)
900                     if label:  self.wallet.labels[address] = label
901                     self.wallet.save()
902                     self.update_sending_tab()
903                 else:
904                     errorDialog = gtk.MessageDialog(
905                         parent=self.window,
906                         flags=gtk.DIALOG_MODAL, 
907                         buttons= gtk.BUTTONS_CLOSE, 
908                         message_format = "Invalid address")
909                     errorDialog.show()
910                     errorDialog.run()
911                     errorDialog.destroy()
912         else:
913                 password = password_dialog() if self.wallet.use_encryption else None
914                 success, ret = self.wallet.get_new_address(password)
915                 self.wallet.new_session() # we created a new address
916                 if success:
917                     address = ret
918                     #if label:  self.wallet.labels[address] = label
919                     self.wallet.save()
920                     self.update_receiving_tab()
921                 else:
922                     msg = ret
923                     errorDialog = gtk.MessageDialog(
924                         parent=self.window,
925                         flags=gtk.DIALOG_MODAL, 
926                         buttons= gtk.BUTTONS_CLOSE, 
927                         message_format = msg)
928                     errorDialog.show()
929                     errorDialog.run()
930                     errorDialog.destroy()
931     
932     def main(self):
933         gtk.main()
934