merge
[electrum-nvc.git] / 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, re
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 import platform
31 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
32
33 from wallet import format_satoshis
34 from interface import DEFAULT_SERVERS
35
36 def numbify(entry, is_int = False):
37     text = entry.get_text().strip()
38     chars = '0123456789'
39     if not is_int: chars +='.'
40     s = ''.join([i for i in text if i in chars])
41     if not is_int:
42         if '.' in s:
43             p = s.find('.')
44             s = s.replace('.','')
45             s = s[:p] + '.' + s[p:p+8]
46         try:
47             amount = int( Decimal(s) * 100000000 )
48         except:
49             amount = None
50     else:
51         try:
52             amount = int( s )
53         except:
54             amount = None
55     entry.set_text(s)
56     return amount
57
58
59
60
61 def show_seed_dialog(wallet, password, parent):
62     import mnemonic
63     try:
64         seed = wallet.pw_decode( wallet.seed, password)
65     except:
66         show_message("Incorrect password")
67         return
68     dialog = gtk.MessageDialog(
69         parent = parent,
70         flags = gtk.DIALOG_MODAL, 
71         buttons = gtk.BUTTONS_OK, 
72         message_format = "Your wallet generation seed is:\n\n" + seed \
73             + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
74             + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
75     dialog.set_title("Seed")
76     dialog.show()
77     dialog.run()
78     dialog.destroy()
79
80 def restore_create_dialog(wallet):
81
82     # ask if the user wants to create a new wallet, or recover from a seed. 
83     # if he wants to recover, and nothing is found, do not create wallet
84     dialog = gtk.Dialog("electrum", parent=None, 
85                         flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, 
86                         buttons= ("create", 0, "restore",1, "cancel",2)  )
87
88     label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?"  )
89     label.show()
90     dialog.vbox.pack_start(label)
91     dialog.show()
92     r = dialog.run()
93     dialog.destroy()
94
95     if r==2: return False
96         
97     is_recovery = (r==1)
98
99     # ask for the server.
100     if not run_network_dialog( wallet, parent=None ): return False
101
102     if not is_recovery:
103
104         wallet.new_seed(None)
105         # generate first key
106         wallet.init_mpk( wallet.seed )
107         wallet.up_to_date_event.clear()
108         wallet.update()
109
110         # run a dialog indicating the seed, ask the user to remember it
111         show_seed_dialog(wallet, None, None)
112             
113         #ask for password
114         change_password_dialog(wallet, None, None)
115     else:
116         # ask for seed and gap.
117         run_recovery_dialog( wallet )
118
119         dialog = gtk.MessageDialog(
120             parent = None,
121             flags = gtk.DIALOG_MODAL, 
122             buttons = gtk.BUTTONS_CANCEL, 
123             message_format = "Please wait..."  )
124         dialog.show()
125
126         def recover_thread( wallet, dialog ):
127             wallet.init_mpk( wallet.seed ) # not encrypted at this point
128             wallet.up_to_date_event.clear()
129             wallet.update()
130
131             if wallet.is_found():
132                 # history and addressbook
133                 wallet.update_tx_history()
134                 wallet.fill_addressbook()
135                 print "recovery successful"
136
137             gobject.idle_add( dialog.destroy )
138
139         thread.start_new_thread( recover_thread, ( wallet, dialog ) )
140         r = dialog.run()
141         dialog.destroy()
142         if r==gtk.RESPONSE_CANCEL: return False
143         if not wallet.is_found:
144             show_message("No transactions found for this seed")
145
146     wallet.save()
147     return True
148
149
150 def run_recovery_dialog(wallet):
151     message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
152     dialog = gtk.MessageDialog(
153         parent = None,
154         flags = gtk.DIALOG_MODAL, 
155         buttons = gtk.BUTTONS_OK_CANCEL,
156         message_format = message)
157
158     vbox = dialog.vbox
159     dialog.set_default_response(gtk.RESPONSE_OK)
160
161     # ask seed, server and gap in the same dialog
162     seed_box = gtk.HBox()
163     seed_label = gtk.Label('Seed or mnemonic:')
164     seed_label.set_size_request(150,-1)
165     seed_box.pack_start(seed_label, False, False, 10)
166     seed_label.show()
167     seed_entry = gtk.Entry()
168     seed_entry.show()
169     seed_entry.set_size_request(450,-1)
170     seed_box.pack_start(seed_entry, False, False, 10)
171     add_help_button(seed_box, '.')
172     seed_box.show()
173     vbox.pack_start(seed_box, False, False, 5)    
174
175     gap = gtk.HBox()
176     gap_label = gtk.Label('Gap limit:')
177     gap_label.set_size_request(150,10)
178     gap_label.show()
179     gap.pack_start(gap_label,False, False, 10)
180     gap_entry = gtk.Entry()
181     gap_entry.set_text("%d"%wallet.gap_limit)
182     gap_entry.connect('changed', numbify, True)
183     gap_entry.show()
184     gap.pack_start(gap_entry,False,False, 10)
185     add_help_button(gap, 'The maximum gap that is allowed between unused addresses in your wallet. During wallet recovery, this parameter is used to decide when to stop the recovery process. If you increase this value, you will need to remember it in order to be able to recover your wallet from seed.')
186     gap.show()
187     vbox.pack_start(gap, False,False, 5)
188
189     dialog.show()
190     r = dialog.run()
191     gap = gap_entry.get_text()        
192     seed = seed_entry.get_text()
193     dialog.destroy()
194
195     if r==gtk.RESPONSE_CANCEL:
196         sys.exit(1)
197     try:
198         gap = int(gap)
199     except:
200         show_message("error")
201         sys.exit(1)
202
203     try:
204         seed.decode('hex')
205     except:
206         import mnemonic
207         print "not hex, trying decode"
208         seed = mnemonic.mn_decode( seed.split(' ') )
209     if not seed:
210         show_message("no seed")
211         sys.exit(1)
212         
213     wallet.seed = seed
214     wallet.gap_limit = gap
215     wallet.save()
216
217
218
219 def run_settings_dialog(wallet, parent):
220
221     message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
222         
223     dialog = gtk.MessageDialog(
224         parent = parent,
225         flags = gtk.DIALOG_MODAL, 
226         buttons = gtk.BUTTONS_OK_CANCEL,
227         message_format = message)
228
229     image = gtk.Image()
230     image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
231     image.show()
232     dialog.set_image(image)
233     dialog.set_title("Settings")
234
235     vbox = dialog.vbox
236     dialog.set_default_response(gtk.RESPONSE_OK)
237
238     fee = gtk.HBox()
239     fee_entry = gtk.Entry()
240     fee_label = gtk.Label('Transaction fee:')
241     fee_label.set_size_request(150,10)
242     fee_label.show()
243     fee.pack_start(fee_label,False, False, 10)
244     fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) )
245     fee_entry.connect('changed', numbify, False)
246     fee_entry.show()
247     fee.pack_start(fee_entry,False,False, 10)
248     add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005')
249     fee.show()
250     vbox.pack_start(fee, False,False, 5)
251             
252     dialog.show()
253     r = dialog.run()
254     fee = fee_entry.get_text()
255         
256     dialog.destroy()
257     if r==gtk.RESPONSE_CANCEL:
258         return
259
260     try:
261         fee = int( 100000000 * Decimal(fee) )
262     except:
263         show_message("error")
264         return
265
266     wallet.fee = fee
267     wallet.save()
268
269
270
271
272 def run_network_dialog( wallet, parent ):
273     image = gtk.Image()
274     image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
275     interface = wallet.interface
276     if parent:
277         if interface.is_connected:
278             status = "Connected to %s:%d\n%d blocks\nresponse time: %f"%(interface.host, interface.port, wallet.blocks, interface.rtime)
279         else:
280             status = "Not connected"
281         server = wallet.server
282     else:
283         import random
284         status = "Please choose a server."
285         server = random.choice( DEFAULT_SERVERS )
286
287     plist = {}
288     for item in wallet.interface.servers:
289         host, pp = item
290         z = {}
291         for item2 in pp:
292             protocol, port = item2
293             z[protocol] = port
294         plist[host] = z
295
296     dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
297                                     gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
298     dialog.set_title("Server")
299     dialog.set_image(image)
300     image.show()
301     
302     vbox = dialog.vbox
303     host_box = gtk.HBox()
304     host_label = gtk.Label('Connect to:')
305     host_label.set_size_request(100,-1)
306     host_label.show()
307     host_box.pack_start(host_label, False, False, 10)
308     host_entry = gtk.Entry()
309     host_entry.set_size_request(200,-1)
310     host_entry.set_text(server)
311     host_entry.show()
312     host_box.pack_start(host_entry, False, False, 10)
313     add_help_button(host_box, 'The name and port number of your Electrum server, separated by a colon. Example: "ecdsa.org:50000". If no port number is provided, port 50000 will be tried. Some servers allow you to connect through http (port 80) or https (port 443)')
314     host_box.show()
315
316
317     p_box = gtk.HBox(False, 10)
318     p_box.show()
319
320     p_label = gtk.Label('Protocol:')
321     p_label.set_size_request(100,-1)
322     p_label.show()
323     p_box.pack_start(p_label, False, False, 10)
324
325     radio1 = gtk.RadioButton(None, "tcp")
326     p_box.pack_start(radio1, True, True, 0)
327     radio1.show()
328     radio2 = gtk.RadioButton(radio1, "http")
329     p_box.pack_start(radio2, True, True, 0)
330     radio2.show()
331     radio3 = gtk.RadioButton(radio1, "native")
332     p_box.pack_start(radio3, True, True, 0)
333     radio3.show()
334
335     def current_line():
336         return unicode(host_entry.get_text()).split(':')
337     
338     def set_button(protocol):
339         if protocol == 't':
340             radio1.set_active(1)
341         elif protocol == 'h':
342             radio2.set_active(1)
343         elif protocol == 'n':
344             radio3.set_active(1)
345
346     def set_protocol(protocol):
347         host = current_line()[0]
348         pp = plist[host]
349         if protocol not in pp.keys():
350             protocol = pp.keys()[0]
351             set_button(protocol)
352         port = pp[protocol]
353         host_entry.set_text( host + ':' + port + ':' + protocol)
354
355     radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1")
356     radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1")
357     radio3.connect("toggled", lambda x,y:set_protocol('n'), "radio button 1")
358         
359     server_list = gtk.ListStore(str)
360     for host in plist.keys():
361         server_list.append([host])
362     
363     treeview = gtk.TreeView(model=server_list)
364     treeview.show()
365
366     tvcolumn = gtk.TreeViewColumn('Active servers')
367     treeview.append_column(tvcolumn)
368     cell = gtk.CellRendererText()
369     tvcolumn.pack_start(cell, False)
370     tvcolumn.add_attribute(cell, 'text', 0)
371
372     scroll = gtk.ScrolledWindow()
373     scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
374     scroll.add(treeview)
375     scroll.show()
376
377     vbox.pack_start(host_box, False,False, 5)
378     vbox.pack_start(p_box, True, True, 0)
379     vbox.pack_start(scroll)
380
381     def my_treeview_cb(treeview):
382         path, view_column = treeview.get_cursor()
383         host = server_list.get_value( server_list.get_iter(path), 0)
384
385         pp = plist[host]
386         if 't' in pp.keys():
387             protocol = 't'
388         else:
389             protocol = pp.keys()[0]
390         port = pp[protocol]
391         host_entry.set_text( host + ':' + port + ':' + protocol)
392         set_button(protocol)
393
394     treeview.connect('cursor-changed', my_treeview_cb)
395
396     dialog.show()
397     r = dialog.run()
398     server = host_entry.get_text()
399     dialog.destroy()
400
401     if r==gtk.RESPONSE_CANCEL:
402         return False
403
404     try:
405         wallet.set_server(server)
406     except:
407         show_message("error:" + server)
408         return False
409
410     if parent:
411         wallet.save()
412     return True
413
414
415
416 def show_message(message, parent=None):
417     dialog = gtk.MessageDialog(
418         parent = parent,
419         flags = gtk.DIALOG_MODAL, 
420         buttons = gtk.BUTTONS_CLOSE, 
421         message_format = message )
422     dialog.show()
423     dialog.run()
424     dialog.destroy()
425
426 def password_line(label):
427     password = gtk.HBox()
428     password_label = gtk.Label(label)
429     password_label.set_size_request(120,10)
430     password_label.show()
431     password.pack_start(password_label,False, False, 10)
432     password_entry = gtk.Entry()
433     password_entry.set_size_request(300,-1)
434     password_entry.set_visibility(False)
435     password_entry.show()
436     password.pack_start(password_entry,False,False, 10)
437     password.show()
438     return password, password_entry
439
440 def password_dialog(parent):
441     dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
442                                 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL,  "Please enter your password.")
443     dialog.get_image().set_visible(False)
444     current_pw, current_pw_entry = password_line('Password:')
445     current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
446     dialog.vbox.pack_start(current_pw, False, True, 0)
447     dialog.show()
448     result = dialog.run()
449     pw = current_pw_entry.get_text()
450     dialog.destroy()
451     if result != gtk.RESPONSE_CANCEL: return pw
452
453 def change_password_dialog(wallet, parent, icon):
454     if parent:
455         msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted'
456     else:
457         msg = "Please choose a password to encrypt your wallet keys"
458
459     dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
460     dialog.set_title("Change password")
461     image = gtk.Image()
462     image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
463     image.show()
464     dialog.set_image(image)
465
466     if wallet.use_encryption:
467         current_pw, current_pw_entry = password_line('Current password:')
468         dialog.vbox.pack_start(current_pw, False, True, 0)
469
470     password, password_entry = password_line('New password:')
471     dialog.vbox.pack_start(password, False, True, 5)
472     password2, password2_entry = password_line('Confirm password:')
473     dialog.vbox.pack_start(password2, False, True, 5)
474
475     dialog.show()
476     result = dialog.run()
477     password = current_pw_entry.get_text() if wallet.use_encryption else None
478     new_password = password_entry.get_text()
479     new_password2 = password2_entry.get_text()
480     dialog.destroy()
481     if result == gtk.RESPONSE_CANCEL: 
482         return
483
484     try:
485         seed = wallet.pw_decode( wallet.seed, password)
486     except:
487         show_message("Incorrect password")
488         return
489
490     if new_password != new_password2:
491         show_message("passwords do not match")
492         return
493
494     wallet.update_password(seed, new_password)
495
496     if icon:
497         if wallet.use_encryption:
498             icon.set_tooltip_text('wallet is encrypted')
499         else:
500             icon.set_tooltip_text('wallet is unencrypted')
501
502
503 def add_help_button(hbox, message):
504     button = gtk.Button('?')
505     button.connect("clicked", lambda x: show_message(message))
506     button.show()
507     hbox.pack_start(button,False, False)
508
509
510 class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
511
512 gobject.type_register(MyWindow)
513 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
514 gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
515
516
517 class ElectrumWindow:
518
519     def show_message(self, msg):
520         show_message(msg, self.window)
521
522     def __init__(self, wallet):
523         self.wallet = wallet
524         self.funds_error = False # True if not enough funds
525
526         self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
527         self.window.set_title(APP_NAME + " " + self.wallet.electrum_version)
528         self.window.connect("destroy", gtk.main_quit)
529         self.window.set_border_width(0)
530         self.window.connect('mykeypress', gtk.main_quit)
531         self.window.set_default_size(720, 350)
532
533         vbox = gtk.VBox()
534
535         self.notebook = gtk.Notebook()
536         self.create_history_tab()
537         self.create_send_tab()
538         self.create_recv_tab()
539         self.create_book_tab()
540         self.create_about_tab()
541         self.notebook.show()
542         vbox.pack_start(self.notebook, True, True, 2)
543         
544         self.status_bar = gtk.Statusbar()
545         vbox.pack_start(self.status_bar, False, False, 0)
546
547         self.status_image = gtk.Image()
548         self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
549         self.status_image.set_alignment(True, 0.5  )
550         self.status_image.show()
551
552         self.network_button = gtk.Button()
553         self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) )
554         self.network_button.add(self.status_image)
555         self.network_button.set_relief(gtk.RELIEF_NONE)
556         self.network_button.show()
557         self.status_bar.pack_end(self.network_button, False, False)
558
559         def seedb(w, wallet):
560             if wallet.use_encryption:
561                 password = password_dialog(self.window)
562                 if not password: return
563             else: password = None
564             show_seed_dialog(wallet, password, self.window)
565         button = gtk.Button('S')
566         button.connect("clicked", seedb, wallet )
567         button.set_relief(gtk.RELIEF_NONE)
568         button.show()
569         self.status_bar.pack_end(button,False, False)
570
571         settings_icon = gtk.Image()
572         settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
573         settings_icon.set_alignment(0.5, 0.5)
574         settings_icon.set_size_request(16,16 )
575         settings_icon.show()
576
577         prefs_button = gtk.Button()
578         prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) )
579         prefs_button.add(settings_icon)
580         prefs_button.set_tooltip_text("Settings")
581         prefs_button.set_relief(gtk.RELIEF_NONE)
582         prefs_button.show()
583         self.status_bar.pack_end(prefs_button,False,False)
584
585         pw_icon = gtk.Image()
586         pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
587         pw_icon.set_alignment(0.5, 0.5)
588         pw_icon.set_size_request(16,16 )
589         pw_icon.show()
590
591         password_button = gtk.Button()
592         password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
593         password_button.add(pw_icon)
594         password_button.set_relief(gtk.RELIEF_NONE)
595         password_button.show()
596         self.status_bar.pack_end(password_button,False,False)
597
598         self.window.add(vbox)
599         self.window.show_all()
600         #self.fee_box.hide()
601
602         self.context_id = self.status_bar.get_context_id("statusbar")
603         self.update_status_bar()
604
605         def update_status_bar_thread():
606             while True:
607                 gobject.idle_add( self.update_status_bar )
608                 time.sleep(0.5)
609
610
611         def check_recipient_thread():
612             old_r = ''
613             while True:
614                 time.sleep(0.5)
615                 if self.payto_entry.is_focus():
616                     continue
617                 r = self.payto_entry.get_text()
618                 if r != old_r:
619                     old_r = r
620                     r = r.strip()
621                     if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
622                         try:
623                             to_address = self.wallet.get_alias(r, interactive=False)
624                         except:
625                             continue
626                         if to_address:
627                             s = r + ' <' + to_address + '>'
628                             gobject.idle_add( lambda: self.payto_entry.set_text(s) )
629                 
630
631         thread.start_new_thread(update_status_bar_thread, ())
632         thread.start_new_thread(check_recipient_thread, ())
633         self.notebook.set_current_page(0)
634
635
636     def add_tab(self, page, name):
637         tab_label = gtk.Label(name)
638         tab_label.show()
639         self.notebook.append_page(page, tab_label)
640
641
642     def create_send_tab(self):
643         
644         page = vbox = gtk.VBox()
645         page.show()
646
647         payto = gtk.HBox()
648         payto_label = gtk.Label('Pay to:')
649         payto_label.set_size_request(100,-1)
650         payto.pack_start(payto_label, False)
651         payto_entry = gtk.Entry()
652         payto_entry.set_size_request(450, 26)
653         payto.pack_start(payto_entry, False)
654         vbox.pack_start(payto, False, False, 5)
655
656         message = gtk.HBox()
657         message_label = gtk.Label('Description:')
658         message_label.set_size_request(100,-1)
659         message.pack_start(message_label, False)
660         message_entry = gtk.Entry()
661         message_entry.set_size_request(450, 26)
662         message.pack_start(message_entry, False)
663         vbox.pack_start(message, False, False, 5)
664
665         amount_box = gtk.HBox()
666         amount_label = gtk.Label('Amount:')
667         amount_label.set_size_request(100,-1)
668         amount_box.pack_start(amount_label, False)
669         amount_entry = gtk.Entry()
670         amount_entry.set_size_request(120, -1)
671         amount_box.pack_start(amount_entry, False)
672         vbox.pack_start(amount_box, False, False, 5)
673
674         self.fee_box = fee_box = gtk.HBox()
675         fee_label = gtk.Label('Fee:')
676         fee_label.set_size_request(100,-1)
677         fee_box.pack_start(fee_label, False)
678         fee_entry = gtk.Entry()
679         fee_entry.set_size_request(60, 26)
680         fee_box.pack_start(fee_entry, False)
681         vbox.pack_start(fee_box, False, False, 5)
682
683         end_box = gtk.HBox()
684         empty_label = gtk.Label('')
685         empty_label.set_size_request(100,-1)
686         end_box.pack_start(empty_label, False)
687         send_button = gtk.Button("Send")
688         send_button.show()
689         end_box.pack_start(send_button, False, False, 0)
690         clear_button = gtk.Button("Clear")
691         clear_button.show()
692         end_box.pack_start(clear_button, False, False, 15)
693         send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
694         clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
695
696         vbox.pack_start(end_box, False, False, 5)
697
698         # display this line only if there is a signature
699         payto_sig = gtk.HBox()
700         payto_sig_id = gtk.Label('')
701         payto_sig.pack_start(payto_sig_id, False)
702         vbox.pack_start(payto_sig, True, True, 5)
703         
704
705         self.user_fee = False
706
707         def entry_changed( entry, is_fee ):
708             self.funds_error = False
709             amount = numbify(amount_entry)
710             fee = numbify(fee_entry)
711             if not is_fee: fee = None
712             if amount is None:
713                 return
714             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
715             if not is_fee:
716                 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
717                 self.fee_box.show()
718             if inputs:
719                 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
720                 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
721                 send_button.set_sensitive(True)
722             else:
723                 send_button.set_sensitive(False)
724                 amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
725                 fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
726                 self.funds_error = True
727
728         amount_entry.connect('changed', entry_changed, False)
729         fee_entry.connect('changed', entry_changed, True)        
730
731         self.payto_entry = payto_entry
732         self.payto_fee_entry = fee_entry
733         self.payto_sig_id = payto_sig_id
734         self.payto_sig = payto_sig
735         self.amount_entry = amount_entry
736         self.message_entry = message_entry
737         self.add_tab(page, 'Send')
738
739     def set_frozen(self,entry,frozen):
740         if frozen:
741             entry.set_editable(False)
742             entry.set_has_frame(False)
743             entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
744         else:
745             entry.set_editable(True)
746             entry.set_has_frame(True)
747             entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
748
749     def set_url(self, url):
750         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
751         self.notebook.set_current_page(1)
752         self.payto_entry.set_text(payto)
753         self.message_entry.set_text(message)
754         self.amount_entry.set_text(amount)
755         if identity:
756             self.set_frozen(self.payto_entry,True)
757             self.set_frozen(self.amount_entry,True)
758             self.set_frozen(self.message_entry,True)
759             self.payto_sig_id.set_text( '      The bitcoin URI was signed by ' + identity )
760         else:
761             self.payto_sig.set_visible(False)
762
763     def create_about_tab(self):
764         import pango
765         page = gtk.VBox()
766         page.show()
767         tv = gtk.TextView()
768         tv.set_editable(False)
769         tv.set_cursor_visible(False)
770         tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
771         page.pack_start(tv)
772         self.info = tv.get_buffer()
773         self.add_tab(page, 'Wall')
774
775     def do_clear(self, w, data):
776         self.payto_sig.set_visible(False)
777         self.payto_fee_entry.set_text('')
778         for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
779             self.set_frozen(entry,False)
780             entry.set_text('')
781
782     def question(self,msg):
783         dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
784         dialog.show()
785         result = dialog.run()
786         dialog.destroy()
787         return result == gtk.RESPONSE_OK
788
789     def do_send(self, w, data):
790         payto_entry, label_entry, amount_entry, fee_entry = data
791         label = label_entry.get_text()
792         r = payto_entry.get_text()
793         r = r.strip()
794
795         m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
796         m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
797         
798         if m1:
799             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
800             if not to_address:
801                 return
802             else:
803                 self.update_sending_tab()
804
805         elif m2:
806             to_address = m2.group(5)
807         else:
808             to_address = r
809
810         if not self.wallet.is_valid(to_address):
811             self.show_message( "invalid bitcoin address:\n"+to_address)
812             return
813
814         try:
815             amount = int( Decimal(amount_entry.get_text()) * 100000000 )
816         except:
817             self.show_message( "invalid amount")
818             return
819         try:
820             fee = int( Decimal(fee_entry.get_text()) * 100000000 )
821         except:
822             self.show_message( "invalid fee")
823             return
824
825         if self.wallet.use_encryption:
826             password = password_dialog(self.window)
827             if not password:
828                 return
829         else:
830             password = None
831
832         try:
833             tx = self.wallet.mktx( to_address, amount, label, password, fee )
834         except BaseException, e:
835             self.show_message(e.message)
836             return
837             
838         status, msg = self.wallet.sendtx( tx )
839         if status:
840             self.show_message( "payment sent.\n" + msg )
841             payto_entry.set_text("")
842             label_entry.set_text("")
843             amount_entry.set_text("")
844             fee_entry.set_text("")
845             #self.fee_box.hide()
846             self.update_sending_tab()
847         else:
848             self.show_message( msg )
849
850
851     def treeview_button_press(self, treeview, event):
852         if event.type == gtk.gdk._2BUTTON_PRESS:
853             c = treeview.get_cursor()[0]
854             if treeview == self.history_treeview:
855                 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
856                 self.show_message(tx_details)
857             elif treeview == self.contacts_treeview:
858                 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
859                 a = self.wallet.aliases.get(m)
860                 if a:
861                     if a[0] in self.wallet.authorities.keys():
862                         s = self.wallet.authorities.get(a[0])
863                     else:
864                         s = "self-signed"
865                     msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
866                     self.show_message(msg)
867             
868
869     def treeview_key_press(self, treeview, event):
870         c = treeview.get_cursor()[0]
871         if event.keyval == gtk.keysyms.Up:
872             if c and c[0] == 0:
873                 treeview.parent.grab_focus()
874                 treeview.set_cursor((0,))
875         elif event.keyval == gtk.keysyms.Return:
876             if treeview == self.history_treeview:
877                 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
878                 self.show_message(tx_details)
879             elif treeview == self.contacts_treeview:
880                 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
881                 a = self.wallet.aliases.get(m)
882                 if a:
883                     if a[0] in self.wallet.authorities.keys():
884                         s = self.wallet.authorities.get(a[0])
885                     else:
886                         s = "self"
887                     msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
888                     self.show_message(msg)
889
890         return False
891
892     def create_history_tab(self):
893
894         self.history_list = gtk.ListStore(str, str, str, str, 'gboolean',  str, str, str, str)
895         treeview = gtk.TreeView(model=self.history_list)
896         self.history_treeview = treeview
897         treeview.set_tooltip_column(7)
898         treeview.show()
899         treeview.connect('key-press-event', self.treeview_key_press)
900         treeview.connect('button-press-event', self.treeview_button_press)
901
902         tvcolumn = gtk.TreeViewColumn('')
903         treeview.append_column(tvcolumn)
904         cell = gtk.CellRendererPixbuf()
905         tvcolumn.pack_start(cell, False)
906         tvcolumn.set_attributes(cell, stock_id=1)
907
908         tvcolumn = gtk.TreeViewColumn('Date')
909         treeview.append_column(tvcolumn)
910         cell = gtk.CellRendererText()
911         tvcolumn.pack_start(cell, False)
912         tvcolumn.add_attribute(cell, 'text', 2)
913
914         tvcolumn = gtk.TreeViewColumn('Description')
915         treeview.append_column(tvcolumn)
916         cell = gtk.CellRendererText()
917         cell.set_property('foreground', 'grey')
918         cell.set_property('family', MONOSPACE_FONT)
919         cell.set_property('editable', True)
920         def edited_cb(cell, path, new_text, h_list):
921             tx = h_list.get_value( h_list.get_iter(path), 0)
922             self.wallet.labels[tx] = new_text
923             self.wallet.save() 
924             self.update_history_tab()
925         cell.connect('edited', edited_cb, self.history_list)
926         def editing_started(cell, entry, path, h_list):
927             tx = h_list.get_value( h_list.get_iter(path), 0)
928             if not self.wallet.labels.get(tx): entry.set_text('')
929         cell.connect('editing-started', editing_started, self.history_list)
930         tvcolumn.set_expand(True)
931         tvcolumn.pack_start(cell, True)
932         tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
933
934         tvcolumn = gtk.TreeViewColumn('Amount')
935         treeview.append_column(tvcolumn)
936         cell = gtk.CellRendererText()
937         cell.set_alignment(1, 0.5)
938         cell.set_property('family', MONOSPACE_FONT)
939         tvcolumn.pack_start(cell, False)
940         tvcolumn.add_attribute(cell, 'text', 5)
941
942         tvcolumn = gtk.TreeViewColumn('Balance')
943         treeview.append_column(tvcolumn)
944         cell = gtk.CellRendererText()
945         cell.set_alignment(1, 0.5)
946         cell.set_property('family', MONOSPACE_FONT)
947         tvcolumn.pack_start(cell, False)
948         tvcolumn.add_attribute(cell, 'text', 6)
949
950         tvcolumn = gtk.TreeViewColumn('Tooltip')
951         treeview.append_column(tvcolumn)
952         cell = gtk.CellRendererText()
953         tvcolumn.pack_start(cell, False)
954         tvcolumn.add_attribute(cell, 'text', 7)
955         tvcolumn.set_visible(False)
956
957         scroll = gtk.ScrolledWindow()
958         scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
959         scroll.add(treeview)
960
961         self.add_tab(scroll, 'History')
962         self.update_history_tab()
963
964
965     def create_recv_tab(self):
966         self.recv_list = gtk.ListStore(str, str, str)
967         self.add_tab( self.make_address_list(True), 'Receive')
968         self.update_receiving_tab()
969
970     def create_book_tab(self):
971         self.addressbook_list = gtk.ListStore(str, str, str)
972         self.add_tab( self.make_address_list(False), 'Contacts')
973         self.update_sending_tab()
974
975     def make_address_list(self, is_recv):
976         liststore = self.recv_list if is_recv else self.addressbook_list
977         treeview = gtk.TreeView(model= liststore)
978         treeview.connect('key-press-event', self.treeview_key_press)
979         treeview.connect('button-press-event', self.treeview_button_press)
980         treeview.show()
981         if not is_recv:
982             self.contacts_treeview = treeview
983
984         tvcolumn = gtk.TreeViewColumn('Address')
985         treeview.append_column(tvcolumn)
986         cell = gtk.CellRendererText()
987         cell.set_property('family', MONOSPACE_FONT)
988         tvcolumn.pack_start(cell, True)
989         tvcolumn.add_attribute(cell, 'text', 0)
990
991         tvcolumn = gtk.TreeViewColumn('Label')
992         tvcolumn.set_expand(True)
993         treeview.append_column(tvcolumn)
994         cell = gtk.CellRendererText()
995         cell.set_property('editable', True)
996         def edited_cb2(cell, path, new_text, liststore):
997             address = liststore.get_value( liststore.get_iter(path), 0)
998             self.wallet.labels[address] = new_text
999             self.wallet.save() 
1000             self.wallet.update_tx_labels()
1001             self.update_receiving_tab()
1002             self.update_sending_tab()
1003             self.update_history_tab()
1004         cell.connect('edited', edited_cb2, liststore)
1005         tvcolumn.pack_start(cell, True)
1006         tvcolumn.add_attribute(cell, 'text', 1)
1007
1008         tvcolumn = gtk.TreeViewColumn('Tx')
1009         treeview.append_column(tvcolumn)
1010         cell = gtk.CellRendererText()
1011         tvcolumn.pack_start(cell, True)
1012         tvcolumn.add_attribute(cell, 'text', 2)
1013
1014         scroll = gtk.ScrolledWindow()
1015         scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1016         scroll.add(treeview)
1017
1018         hbox = gtk.HBox()
1019         if not is_recv:
1020             button = gtk.Button("New")
1021             button.connect("clicked", self.newaddress_dialog)
1022             button.show()
1023             hbox.pack_start(button,False)
1024
1025         def showqrcode(w, treeview, liststore):
1026             path, col = treeview.get_cursor()
1027             if not path: return
1028             address = liststore.get_value(liststore.get_iter(path), 0)
1029             qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1030             qr.addData(address)
1031             qr.make()
1032             boxsize = 7
1033             size = qr.getModuleCount()*boxsize
1034             def area_expose_cb(area, event):
1035                 style = area.get_style()
1036                 k = qr.getModuleCount()
1037                 for r in range(k):
1038                     for c in range(k):
1039                         gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1040                         area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1041             area = gtk.DrawingArea()
1042             area.set_size_request(size, size)
1043             area.connect("expose-event", area_expose_cb)
1044             area.show()
1045             dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1046             dialog.vbox.add(area)
1047             dialog.run()
1048             dialog.destroy()
1049
1050         button = gtk.Button("QR")
1051         button.connect("clicked", showqrcode, treeview, liststore)
1052         button.show()
1053         hbox.pack_start(button,False)
1054
1055         button = gtk.Button("Copy to clipboard")
1056         def copy2clipboard(w, treeview, liststore):
1057             import platform
1058             path, col =  treeview.get_cursor()
1059             if path:
1060                 address =  liststore.get_value( liststore.get_iter(path), 0)
1061                 if platform.system() == 'Windows':
1062                     from Tkinter import Tk
1063                     r = Tk()
1064                     r.withdraw()
1065                     r.clipboard_clear()
1066                     r.clipboard_append( address )
1067                     r.destroy()
1068                 else:
1069                     c = gtk.clipboard_get()
1070                     c.set_text( address )
1071         button.connect("clicked", copy2clipboard, treeview, liststore)
1072         button.show()
1073         hbox.pack_start(button,False)
1074
1075         if not is_recv:
1076             button = gtk.Button("Pay to")
1077             def payto(w, treeview, liststore):
1078                 path, col =  treeview.get_cursor()
1079                 if path:
1080                     address =  liststore.get_value( liststore.get_iter(path), 0)
1081                     self.payto_entry.set_text( address )
1082                     self.notebook.set_current_page(1)
1083                     self.amount_entry.grab_focus()
1084
1085             button.connect("clicked", payto, treeview, liststore)
1086             button.show()
1087             hbox.pack_start(button,False)
1088
1089         vbox = gtk.VBox()
1090         vbox.pack_start(scroll,True)
1091         vbox.pack_start(hbox, False)
1092         return vbox
1093
1094     def update_status_bar(self):
1095         interface = self.wallet.interface
1096         if self.funds_error:
1097             text = "Not enough funds"
1098         elif interface.is_connected:
1099             self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks\nresponse time: %f"%(interface.host, interface.port, self.wallet.blocks, interface.rtime))
1100             if self.wallet.blocks == -1:
1101                 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1102                 text = "Connecting..."
1103             elif self.wallet.blocks == 0:
1104                 self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1105                 text = "Server not ready"
1106             elif not self.wallet.up_to_date:
1107                 self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1108                 text = "Synchronizing..."
1109             else:
1110                 self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1111                 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks\nresponse time: %f"%(interface.host, interface.port, self.wallet.blocks, interface.rtime))
1112                 c, u = self.wallet.get_balance()
1113                 text =  "Balance: %s "%( format_satoshis(c) )
1114                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True).strip() )
1115         else:
1116             self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1117             self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(interface.host, self.wallet.blocks))
1118             text = "Not connected"
1119
1120         self.status_bar.pop(self.context_id) 
1121         self.status_bar.push(self.context_id, text)
1122
1123         if self.wallet.was_updated and self.wallet.up_to_date:
1124             self.update_history_tab()
1125             self.update_receiving_tab()
1126             # addressbook too...
1127             self.info.set_text( self.wallet.banner )
1128             self.wallet.was_updated = False
1129         
1130
1131     def update_receiving_tab(self):
1132         self.recv_list.clear()
1133         for address in self.wallet.all_addresses():
1134             if self.wallet.is_change(address):continue
1135             label = self.wallet.labels.get(address)
1136             n = 0 
1137             h = self.wallet.history.get(address,[])
1138             for item in h:
1139                 if not item['is_input'] : n=n+1
1140             tx = "None" if n==0 else "%d"%n
1141             self.recv_list.append((address, label, tx ))
1142
1143     def update_sending_tab(self):
1144         # detect addresses that are not mine in history, add them here...
1145         self.addressbook_list.clear()
1146         for alias, v in self.wallet.aliases.items():
1147             s, target = v
1148             label = self.wallet.labels.get(alias)
1149             self.addressbook_list.append((alias, label, '-'))
1150             
1151         for address in self.wallet.addressbook:
1152             label = self.wallet.labels.get(address)
1153             n = 0 
1154             for item in self.wallet.tx_history.values():
1155                 if address in item['outputs'] : n=n+1
1156             tx = "None" if n==0 else "%d"%n
1157             self.addressbook_list.append((address, label, tx))
1158
1159     def update_history_tab(self):
1160         cursor = self.history_treeview.get_cursor()[0]
1161         self.history_list.clear()
1162         balance = 0 
1163         for tx in self.wallet.get_tx_history():
1164             tx_hash = tx['tx_hash']
1165             if tx['height']:
1166                 conf = self.wallet.blocks - tx['height'] + 1
1167                 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
1168                 conf_icon = gtk.STOCK_APPLY
1169             else:
1170                 conf = 0
1171                 time_str = 'pending'
1172                 conf_icon = gtk.STOCK_EXECUTE
1173             v = tx['value']
1174             balance += v 
1175             label = self.wallet.labels.get(tx_hash)
1176             is_default_label = (label == '') or (label is None)
1177             if is_default_label: label = tx['default_label']
1178             tooltip = tx_hash + "\n%d confirmations"%conf 
1179
1180             # tx = self.wallet.tx_history.get(tx_hash)
1181             details = "Transaction Details:\n\n" \
1182                       + "Transaction ID:\n" + tx_hash + "\n\n" \
1183                       + "Status: %d confirmations\n\n"%conf  \
1184                       + "Date: %s\n\n"%time_str \
1185                       + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
1186                       + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
1187             r = self.wallet.receipts.get(tx_hash)
1188             if r:
1189                 details += "\n_______________________________________" \
1190                            + '\n\nSigned URI: ' + r[2] \
1191                            + "\n\nSigned by: " + r[0] \
1192                            + '\n\nSignature: ' + r[1]
1193                 
1194
1195             self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1196                                         format_satoshis(v,True), format_satoshis(balance), tooltip, details] )
1197         if cursor: self.history_treeview.set_cursor( cursor )
1198
1199
1200
1201     def newaddress_dialog(self, w):
1202
1203         title = "New Contact" 
1204         dialog = gtk.Dialog(title, parent=self.window, 
1205                             flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, 
1206                             buttons= ("cancel", 0, "ok",1)  )
1207         dialog.show()
1208
1209         label = gtk.HBox()
1210         label_label = gtk.Label('Label:')
1211         label_label.set_size_request(120,10)
1212         label_label.show()
1213         label.pack_start(label_label)
1214         label_entry = gtk.Entry()
1215         label_entry.show()
1216         label.pack_start(label_entry)
1217         label.show()
1218         dialog.vbox.pack_start(label, False, True, 5)
1219
1220         address = gtk.HBox()
1221         address_label = gtk.Label('Address:')
1222         address_label.set_size_request(120,10)
1223         address_label.show()
1224         address.pack_start(address_label)
1225         address_entry = gtk.Entry()
1226         address_entry.show()
1227         address.pack_start(address_entry)
1228         address.show()
1229         dialog.vbox.pack_start(address, False, True, 5)
1230         
1231         result = dialog.run()
1232         address = address_entry.get_text()
1233         label = label_entry.get_text()
1234         dialog.destroy()
1235
1236         if result == 1:
1237             if self.wallet.is_valid(address):
1238                 self.wallet.addressbook.append(address)
1239                 if label:  self.wallet.labels[address] = label
1240                 self.wallet.save()
1241                 self.update_sending_tab()
1242             else:
1243                 errorDialog = gtk.MessageDialog(
1244                     parent=self.window,
1245                     flags=gtk.DIALOG_MODAL, 
1246                     buttons= gtk.BUTTONS_CLOSE, 
1247                     message_format = "Invalid address")
1248                 errorDialog.show()
1249                 errorDialog.run()
1250                 errorDialog.destroy()
1251
1252     
1253
1254 class ElectrumGui():
1255
1256     def __init__(self, wallet):
1257         self.wallet = wallet
1258
1259     def main(self, url=None):
1260         ew = ElectrumWindow(self.wallet)
1261         if url: ew.set_url(url)
1262         gtk.main()
1263
1264     def restore_or_create(self):
1265         return restore_create_dialog(self.wallet)