ef46fe2e9b83744b0c12bef7a1000a68102c1ab9
[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 from decimal import Decimal
27
28 gtk.gdk.threads_init()
29 APP_NAME = "Electrum"
30
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"
37     return xx
38
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.'])
42     if not is_int:
43         p = s.find(".")
44         s = s[:p+9]
45         try:
46             amount = int( Decimal(s) * 100000000 )
47         except:
48             amount = None
49     entry.set_text(s)
50     return amount
51
52
53
54
55 def show_seed_dialog(wallet, password, parent):
56     import mnemonic
57     try:
58         seed = wallet.pw_decode( wallet.seed, password)
59         private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
60     except:
61         show_message("Incorrect password")
62         return
63     dialog = gtk.MessageDialog(
64         parent = parent,
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")
71     dialog.show()
72     dialog.run()
73     dialog.destroy()
74
75 def init_wallet(wallet):
76
77     try:
78         found = wallet.read()
79     except BaseException, e:
80         show_message(e.message)
81         found = 1
82
83     if not found: 
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)  )
89
90         label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?"  )
91         label.show()
92         dialog.vbox.pack_start(label)
93         dialog.show()
94         r = dialog.run()
95         dialog.destroy()
96         if r==2:
97             sys.exit(1)
98         
99         is_recovery = (r==1)
100
101         if not is_recovery:
102
103             wallet.new_seed(None)
104
105             # ask for the server.
106             run_settings_dialog(wallet, is_create=True, is_recovery=False, parent=None)
107
108             # generate first key
109             wallet.create_new_address(False, None)
110
111             # run a dialog indicating the seed, ask the user to remember it
112             show_seed_dialog(wallet, None, None)
113             
114             #ask for password
115             change_password_dialog(wallet, None, None)
116
117         else:
118             # ask for the server, seed and gap.
119             run_settings_dialog(wallet, is_create=True, is_recovery=True, parent=None)
120
121             dialog = gtk.MessageDialog(
122                 parent = None,
123                 flags = gtk.DIALOG_MODAL, 
124                 buttons = gtk.BUTTONS_CANCEL, 
125                 message_format = "Please wait..."  )
126             dialog.show()
127
128             def recover_thread( wallet, dialog, password ):
129                 wallet.is_found = wallet.recover( password )
130                 if wallet.is_found:
131                     wallet.save()
132                 gobject.idle_add( dialog.destroy )
133
134             thread.start_new_thread( recover_thread, ( wallet, dialog, None ) ) # no password
135             r = dialog.run()
136             dialog.destroy()
137             if r==gtk.RESPONSE_CANCEL: sys.exit(1)
138             if not wallet.is_found:
139                 show_message("No transactions found for this seed")
140
141
142 def run_settings_dialog(wallet, is_create, is_recovery, parent):
143
144     if is_recovery:
145         message = "Please enter your wallet seed or the corresponding mnemonic list of words, the server and the gap limit"
146     elif is_create:
147         message = "Please indicate the server and port number"
148     else:
149         message = "These are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
150         
151     dialog = gtk.MessageDialog(
152         parent = parent,
153         flags = gtk.DIALOG_MODAL, 
154         buttons = gtk.BUTTONS_OK_CANCEL,
155         message_format = message)
156
157     image = gtk.Image()
158     image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
159     image.show()
160     dialog.set_image(image)
161     dialog.set_title("Settings")
162
163     vbox = dialog.vbox
164     dialog.set_default_response(gtk.RESPONSE_OK)
165
166     if is_recovery:
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)
172         seed_label.show()
173         seed_entry = gtk.Entry()
174         seed_entry.show()
175         seed_entry.set_size_request(450,-1)
176         seed_box.pack_start(seed_entry, False, False, 10)
177         add_help_button(seed_box, '.')
178         seed_box.show()
179         vbox.pack_start(seed_box, False, False, 5)    
180
181     if is_recovery or (not is_create):
182         gap = gtk.HBox()
183         gap_label = gtk.Label('Gap limit:')
184         gap_label.set_size_request(150,10)
185         gap_label.show()
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)
190         gap_entry.show()
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.')
193         gap.show()
194         vbox.pack_start(gap, False,False, 5)
195
196     if is_recovery or is_create:
197         host = gtk.HBox()
198         host_label = gtk.Label('Server:')
199         host_label.set_size_request(150,-1)
200         host_label.show()
201         host.pack_start(host_label,False, False, 10)
202         host_entry = gtk.Entry()
203         host_entry.set_text(wallet.host+":%d"%wallet.port)
204         host_entry.show()
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.')
207         host.show()
208         vbox.pack_start(host, False,False, 5)
209
210     if not is_create:
211         fee = gtk.HBox()
212         fee_entry = gtk.Entry()
213         fee_label = gtk.Label('Transaction fee:')
214         fee_label.set_size_request(150,10)
215         fee_label.show()
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)
219         fee_entry.show()
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')
222         fee.show()
223         vbox.pack_start(fee, False,False, 5)
224             
225
226     dialog.show()
227     r = dialog.run()
228     if is_create:
229         hh = host_entry.get_text()
230     if is_recovery:
231         gap = gap_entry.get_text()
232         seed = seed_entry.get_text()
233         try:
234             seed.decode('hex')
235         except:
236             import mnemonic
237             print "not hex, trying decode"
238             seed = mnemonic.mn_decode( seed.split(' ') )
239     if not is_create:
240         fee = fee_entry.get_text()
241         gap = gap_entry.get_text()
242         
243     dialog.destroy()
244     if r==gtk.RESPONSE_CANCEL:
245         if is_create: sys.exit(1)
246         else: return
247
248     try:
249         if is_create:
250             if ':' in hh:
251                 host, port = hh.split(':')
252                 port = int(port)
253             else:
254                 host = hh
255                 port = 50000
256         if is_recovery:
257             gap = int(gap)
258         if not is_create:
259             fee = int( 100000000 * Decimal(fee) )
260             gap = int(gap)
261     except:
262         show_message("error")
263         return
264
265     if is_create:
266         wallet.host = host
267         wallet.port = port
268     if is_recovery:
269         wallet.seed = seed
270         wallet.gap_limit = gap
271     if not is_create:
272         wallet.fee = fee
273         wallet.gap_limit = gap
274     wallet.save()
275
276
277
278
279 def show_message(message, parent=None):
280     dialog = gtk.MessageDialog(
281         parent = parent,
282         flags = gtk.DIALOG_MODAL, 
283         buttons = gtk.BUTTONS_CLOSE, 
284         message_format = message )
285     dialog.show()
286     dialog.run()
287     dialog.destroy()
288
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)
300     password.show()
301     return password, password_entry
302
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)
310     dialog.show()
311     result = dialog.run()
312     pw = current_pw_entry.get_text()
313     dialog.destroy()
314     if result: return pw
315
316 def change_password_dialog(wallet, parent, icon):
317     if parent:
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'
319     else:
320         msg = "Please choose a password to encrypt your wallet keys"
321
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")
324     image = gtk.Image()
325     image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
326     image.show()
327     dialog.set_image(image)
328
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)
332
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)
337
338     dialog.show()
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()
343     dialog.destroy()
344     if result == gtk.RESPONSE_CANCEL: 
345         return
346
347     try:
348         seed = wallet.pw_decode( wallet.seed, password)
349         private_keys = ast.literal_eval( wallet.pw_decode( wallet.private_keys, password) )
350     except:
351         show_message("Incorrect password")
352         return
353
354     if new_password != new_password2:
355         show_message("passwords do not match")
356         return
357
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)
361     wallet.save()
362
363     if icon:
364         if wallet.use_encryption:
365             icon.set_tooltip_text('wallet is encrypted')
366         else:
367             icon.set_tooltip_text('wallet is unencrypted')
368
369
370 def add_help_button(hbox, message):
371     button = gtk.Button('?')
372     button.connect("clicked", lambda x: show_message(message))
373     button.show()
374     hbox.pack_start(button,False, False)
375
376
377 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
378
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')
382
383
384 class BitcoinGUI:
385
386     def show_message(self, msg):
387         show_message(msg, self.window)
388
389     def __init__(self, wallet):
390         self.error = ''
391         self.is_connected = False
392         self.wallet = wallet
393         self.period = 5
394
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)
401
402         vbox = gtk.VBox()
403
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()
410         self.notebook.show()
411         vbox.pack_start(self.notebook, True, True, 2)
412         
413         self.status_bar = gtk.Statusbar()
414         vbox.pack_start(self.status_bar, False, False, 0)
415
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()
420
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)
427
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)
437         button.show()
438         self.status_bar.pack_end(button,False, False)
439
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 )
444         settings_icon.show()
445
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)
451         prefs_button.show()
452         self.status_bar.pack_end(prefs_button,False,False)
453
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 )
458         pw_icon.show()
459
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)
466
467         self.window.add(vbox)
468         self.window.show_all()
469         self.fee_box.hide()
470
471         self.context_id = self.status_bar.get_context_id("statusbar")
472         self.update_status_bar()
473
474         def update_status_bar_thread():
475             while True:
476                 gobject.idle_add( self.update_status_bar )
477                 time.sleep(0.5)
478
479         def update_wallet_thread():
480             while True:
481                 try:
482                     self.wallet.new_session()
483                     self.is_connected = True
484                     self.info.set_text( self.wallet.message)
485                 except:
486                     self.is_connected = False
487                     traceback.print_exc(file=sys.stdout)
488                     time.sleep(self.period)
489                     continue
490
491                 get_servers_time = 0
492                 while True:
493                     if time.time() - get_servers_time > 5*60:
494                         wallet.get_servers()
495                         get_servers_time = time.time()
496                         
497                     self.period = 15 if self.wallet.use_http() else 5
498                     try:
499                         u = self.wallet.update()
500                         self.is_connected = True
501                     except BaseException:
502                         print "starting new session"
503                         break
504                     except socket.gaierror:
505                         self.is_connected = False
506                         break
507                     except:
508                         self.is_connected = False
509                         print "error"
510                         traceback.print_exc(file=sys.stdout)
511                         break
512                     self.error = '' if self.is_connected else "Not connected"
513                     if u:
514                         self.wallet.save()
515                         gobject.idle_add( self.update_history_tab )
516                     time.sleep(self.period)
517                     
518         thread.start_new_thread(update_wallet_thread, ())
519         thread.start_new_thread(update_status_bar_thread, ())
520         self.notebook.set_current_page(0)
521
522
523     def add_tab(self, page, name):
524         tab_label = gtk.Label(name)
525         tab_label.show()
526         self.notebook.append_page(page, tab_label)
527
528
529     def create_send_tab(self):
530
531         page = vbox = gtk.VBox()
532         page.show()
533
534         payto = gtk.HBox()
535         payto_label = gtk.Label('Pay to:')
536         payto_label.set_size_request(100,10)
537         payto_label.show()
538         payto.pack_start(payto_label, False)
539         payto_entry = gtk.Entry()
540         payto_entry.set_size_request(350, 26)
541         payto_entry.show()
542         payto.pack_start(payto_entry, False)
543         vbox.pack_start(payto, False, False, 5)
544         
545         label = gtk.HBox()
546         label_label = gtk.Label('Label:')
547         label_label.set_size_request(100,10)
548         label_label.show()
549         label.pack_start(label_label, False)
550         label_entry = gtk.Entry()
551         label_entry.set_size_request(350, 26)
552         label_entry.show()
553         label.pack_start(label_entry, False)
554         vbox.pack_start(label, False, False, 5)
555
556         amount_box = gtk.HBox()
557         amount_label = gtk.Label('Amount:')
558         amount_label.set_size_request(100,-1)
559         amount_label.show()
560         amount_box.pack_start(amount_label, False)
561         amount_entry = gtk.Entry()
562         amount_entry.set_size_request(120, -1)
563         amount_entry.show()
564         amount_box.pack_start(amount_entry, False)
565         vbox.pack_start(amount_box, False, False, 5)
566
567         send_button = gtk.Button("Send")
568         send_button.show()
569         amount_box.pack_start(send_button, False, False, 5)
570
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)
580
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)
583
584         self.user_fee = False
585
586         def entry_changed( entry, is_fee ):
587             amount = numbify(amount_entry)
588             fee = numbify(fee_entry)
589             if not is_fee: fee = None
590             if amount is None: 
591                 self.fee_box.hide(); return
592             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
593             if not is_fee:
594                 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
595                 self.fee_box.show()
596             if inputs:
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)
600                 self.error = ''
601             else:
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'
606
607         amount_entry.connect('changed', entry_changed, False)
608         fee_entry.connect('changed', entry_changed, True)
609
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')
614
615     def create_about_tab(self):
616         page = gtk.VBox()
617         page.show()
618         #self.info = gtk.Label('')  
619         #self.info.set_selectable(True)
620         #page.pack_start(self.info)
621         tv = gtk.TextView()
622         tv.set_editable(False)
623         tv.set_cursor_visible(False)
624         page.pack_start(tv)
625         self.info = tv.get_buffer()
626         self.add_tab(page, 'Wall')
627
628     def do_send(self, w, data):
629         payto_entry, label_entry, amount_entry, fee_entry = data
630         
631         label = label_entry.get_text()
632
633         to_address = payto_entry.get_text()
634         if not self.wallet.is_valid(to_address):
635             self.show_message( "invalid bitcoin address")
636             return
637
638         try:
639             amount = int( Decimal(amount_entry.get_text()) * 100000000 )
640         except:
641             self.show_message( "invalid amount")
642             return
643         try:
644             fee = int( Decimal(fee_entry.get_text()) * 100000000 )
645         except:
646             self.show_message( "invalid fee")
647             return
648
649         password = password_dialog() if self.wallet.use_encryption else None
650
651         status, tx = self.wallet.mktx( to_address, amount, label, password, fee )
652         self.wallet.new_session() # we created a new change address
653         if not status:
654             self.show_message(tx)
655             return
656
657         status, msg = self.wallet.sendtx( tx )
658         if status:
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("")
664             self.fee_box.hide()
665             self.update_sending_tab()
666         else:
667             self.show_message( msg )
668
669
670     def treeview_key_press(self, treeview, event):
671         c = treeview.get_cursor()[0]
672         if event.keyval == gtk.keysyms.Up:
673             if c and c[0] == 0:
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)
679         return False
680
681     def create_history_tab(self):
682
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)
687         treeview.show()
688         treeview.connect('key-press-event', self.treeview_key_press)
689
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)
695
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)
701
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
711             self.wallet.save() 
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)
721
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)
728
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)
735
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)
742
743         scroll = gtk.ScrolledWindow()
744         scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
745         scroll.add(treeview)
746
747         self.add_tab(scroll, 'History')
748         self.update_history_tab()
749
750
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()
755
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()
760
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)
765         treeview.show()
766
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)
773
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
782             self.wallet.save() 
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)
790
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)
796
797         scroll = gtk.ScrolledWindow()
798         scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
799         scroll.add(treeview)
800
801         hbox = gtk.HBox()
802         button = gtk.Button("New address")
803         button.connect("clicked", self.newaddress_dialog, is_recv)
804         button.show()
805         hbox.pack_start(button,False)
806
807         def showqrcode(w, treeview, liststore):
808             path, col = treeview.get_cursor()
809             if not path: return
810             address = liststore.get_value(liststore.get_iter(path), 0)
811             qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
812             qr.addData(address)
813             qr.make()
814             boxsize = 7
815             size = qr.getModuleCount()*boxsize
816             def area_expose_cb(area, event):
817                 style = area.get_style()
818                 k = qr.getModuleCount()
819                 for r in range(k):
820                     for c in range(k):
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)
826             area.show()
827             dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
828             dialog.vbox.add(area)
829             dialog.run()
830             dialog.destroy()
831
832         button = gtk.Button("QR")
833         button.connect("clicked", showqrcode, treeview, liststore)
834         button.show()
835         hbox.pack_start(button,False)
836
837         button = gtk.Button("Copy to clipboard")
838         def copy2clipboard(w, treeview, liststore):
839             import platform
840             path, col =  treeview.get_cursor()
841             if path:
842                 address =  liststore.get_value( liststore.get_iter(path), 0)
843                 if platform.system() == 'Windows':
844                     from Tkinter import Tk
845                     r = Tk()
846                     r.withdraw()
847                     r.clipboard_clear()
848                     r.clipboard_append( address )
849                     r.destroy()
850                 else:
851                     c = gtk.clipboard_get()
852                     c.set_text( address )
853         button.connect("clicked", copy2clipboard, treeview, liststore)
854         button.show()
855         hbox.pack_start(button,False)
856
857         if not is_recv:
858             button = gtk.Button("Pay to")
859             def payto(w, treeview, liststore):
860                 path, col =  treeview.get_cursor()
861                 if path:
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()
866
867             button.connect("clicked", payto, treeview, liststore)
868             button.show()
869             hbox.pack_start(button,False)
870
871         vbox = gtk.VBox()
872         vbox.pack_start(scroll,True)
873         vbox.pack_start(hbox, False)
874         return vbox
875
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))
881         else:
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) 
889
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)
895             n = 0 
896             h = self.wallet.history.get(address)
897             if h:
898                 for item in h:
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 ))
902
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)
908             n = 0 
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))
913
914     def update_history_tab(self):
915         cursor = self.history_treeview.get_cursor()[0]
916         self.history_list.clear()
917         balance = 0 
918         for tx in self.wallet.get_tx_history():
919             tx_hash = tx['tx_hash']
920             if tx['height']:
921                 conf = self.wallet.blocks - tx['height'] + 1
922                 time_str = datetime.datetime.fromtimestamp( tx['nTime']).isoformat(' ')[:-3]
923                 conf_icon = gtk.STOCK_APPLY
924             else:
925                 conf = 0
926                 time_str = 'pending'
927                 conf_icon = gtk.STOCK_EXECUTE
928             v = tx['value']
929             balance += v 
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 
934
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'])
942
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 )
946
947
948
949     def newaddress_dialog(self, w, is_recv):
950
951         if not is_recv:
952
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)  )
957             dialog.show()
958
959             label = gtk.HBox()
960             label_label = gtk.Label('Label:')
961             label_label.set_size_request(120,10)
962             label_label.show()
963             label.pack_start(label_label)
964             label_entry = gtk.Entry()
965             label_entry.show()
966             label.pack_start(label_entry)
967             label.show()
968             dialog.vbox.pack_start(label, False, True, 5)
969
970             address = gtk.HBox()
971             address_label = gtk.Label('Address:')
972             address_label.set_size_request(120,10)
973             address_label.show()
974             address.pack_start(address_label)
975             address_entry = gtk.Entry()
976             address_entry.show()
977             address.pack_start(address_entry)
978             address.show()
979             dialog.vbox.pack_start(address, False, True, 5)
980
981             result = dialog.run()
982             address = address_entry.get_text()
983             label = label_entry.get_text()
984             dialog.destroy()
985
986             if result == 1:
987                 if self.wallet.is_valid(address):
988                     self.wallet.addressbook.append(address)
989                     if label:  self.wallet.labels[address] = label
990                     self.wallet.save()
991                     self.update_sending_tab()
992                 else:
993                     errorDialog = gtk.MessageDialog(
994                         parent=self.window,
995                         flags=gtk.DIALOG_MODAL, 
996                         buttons= gtk.BUTTONS_CLOSE, 
997                         message_format = "Invalid address")
998                     errorDialog.show()
999                     errorDialog.run()
1000                     errorDialog.destroy()
1001         else:
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
1005                 if success:
1006                     address = ret
1007                     #if label:  self.wallet.labels[address] = label
1008                     self.wallet.save()
1009                     self.update_receiving_tab()
1010                 else:
1011                     msg = ret
1012                     errorDialog = gtk.MessageDialog(
1013                         parent=self.window,
1014                         flags=gtk.DIALOG_MODAL, 
1015                         buttons= gtk.BUTTONS_CLOSE, 
1016                         message_format = msg)
1017                     errorDialog.show()
1018                     errorDialog.run()
1019                     errorDialog.destroy()
1020     
1021     def network_dialog( self, w ):
1022         wallet = self.wallet
1023         image = gtk.Image()
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)
1027         else:
1028             status = "Not connected"
1029
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)
1034         image.show()
1035     
1036         vbox = dialog.vbox
1037         host = gtk.HBox()
1038         host_label = gtk.Label('Connect to:')
1039         host_label.set_size_request(100,-1)
1040         host_label.show()
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)
1045         host_entry.show()
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)')
1048         host.show()
1049         
1050         server_list = gtk.ListStore(str)
1051         for item in wallet.servers:
1052             server_list.append([item])
1053     
1054         treeview = gtk.TreeView(model=server_list)
1055         treeview.show()
1056
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)
1062
1063         scroll = gtk.ScrolledWindow()
1064         scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1065         scroll.add(treeview)
1066         scroll.show()
1067
1068         vbox.pack_start(host, False,False, 5)
1069         vbox.pack_start(scroll)
1070
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)
1076
1077         dialog.show()
1078         r = dialog.run()
1079         hh = host_entry.get_text()
1080         dialog.destroy()
1081         if r==gtk.RESPONSE_CANCEL:
1082             return
1083         print hh
1084         try:
1085             if ':' in hh:
1086                 host, port = hh.split(':')
1087                 port = int(port)
1088             else:
1089                 host = hh
1090                 port = 50000
1091         except:
1092             self.show_message("error")
1093             return
1094
1095         if host!= wallet.host or port!=wallet.port:
1096             wallet.host = host
1097             wallet.port = port
1098             wallet.save()
1099             self.is_connected = False
1100
1101
1102     def main(self):
1103         gtk.main()
1104