follow up to #604 - gtk gui
[electrum-nvc.git] / gui / gtk.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
20 import datetime
21 import thread, time, ast, sys, re
22 import socket, traceback
23 import gi
24 gi.require_version('Gtk', '3.0')
25 from gi.repository import Gtk, Gdk, GObject, cairo
26 from decimal import Decimal
27 from electrum.util import print_error
28 from electrum.bitcoin import is_valid
29 from electrum import mnemonic, pyqrnative, WalletStorage, Wallet
30
31 Gdk.threads_init()
32 APP_NAME = "Electrum"
33 import platform
34 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
35
36 from electrum.util import format_satoshis
37 from electrum.network import DEFAULT_SERVERS
38 from electrum.bitcoin import MIN_RELAY_TX_FEE
39
40 def numbify(entry, is_int = False):
41     text = entry.get_text().strip()
42     chars = '0123456789'
43     if not is_int: chars +='.'
44     s = ''.join([i for i in text if i in chars])
45     if not is_int:
46         if '.' in s:
47             p = s.find('.')
48             s = s.replace('.','')
49             s = s[:p] + '.' + s[p:p+8]
50         try:
51             amount = int( Decimal(s) * 100000000 )
52         except Exception:
53             amount = None
54     else:
55         try:
56             amount = int( s )
57         except Exception:
58             amount = None
59     entry.set_text(s)
60     return amount
61
62
63
64
65 def show_seed_dialog(wallet, password, parent):
66     if not wallet.seed:
67         show_message("No seed")
68         return
69     try:
70         mnemonic = wallet.get_mnemonic(password)
71     except Exception:
72         show_message("Incorrect password")
73         return
74     dialog = Gtk.MessageDialog(
75         parent = parent,
76         flags = Gtk.DialogFlags.MODAL, 
77         buttons = Gtk.ButtonsType.OK, 
78         message_format = "Your wallet generation seed is:\n\n" + '"' + mnemonic + '"'\
79             + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" )
80     dialog.set_title("Seed")
81     dialog.show()
82     dialog.run()
83     dialog.destroy()
84
85 def restore_create_dialog():
86
87     # ask if the user wants to create a new wallet, or recover from a seed. 
88     # if he wants to recover, and nothing is found, do not create wallet
89     dialog = Gtk.Dialog("electrum", parent=None, 
90                         flags=Gtk.DialogFlags.MODAL|Gtk.DialogFlags.NO_SEPARATOR, 
91                         buttons= ("create", 0, "restore",1, "cancel",2)  )
92
93     label = Gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?"  )
94     label.show()
95     dialog.vbox.pack_start(label, True, True, 0)
96     dialog.show()
97     r = dialog.run()
98     dialog.destroy()
99
100     if r==2: return False
101     return 'restore' if r==1 else 'create'
102
103
104
105 def run_recovery_dialog():
106     message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
107     dialog = Gtk.MessageDialog(
108         parent = None,
109         flags = Gtk.DialogFlags.MODAL, 
110         buttons = Gtk.ButtonsType.OK_CANCEL,
111         message_format = message)
112
113     vbox = dialog.vbox
114     dialog.set_default_response(Gtk.ResponseType.OK)
115
116     # ask seed, server and gap in the same dialog
117     seed_box = Gtk.HBox()
118     seed_label = Gtk.Label(label='Seed or mnemonic:')
119     seed_label.set_size_request(150,-1)
120     seed_box.pack_start(seed_label, False, False, 10)
121     seed_label.show()
122     seed_entry = Gtk.Entry()
123     seed_entry.show()
124     seed_entry.set_size_request(450,-1)
125     seed_box.pack_start(seed_entry, False, False, 10)
126     add_help_button(seed_box, '.')
127     seed_box.show()
128     vbox.pack_start(seed_box, False, False, 5)    
129
130     dialog.show()
131     r = dialog.run()
132     seed = seed_entry.get_text()
133     dialog.destroy()
134
135     if r==Gtk.ResponseType.CANCEL:
136         return False
137
138     try:
139         seed.decode('hex')
140     except Exception:
141         print_error("Warning: Not hex, trying decode")
142         seed = mnemonic.mn_decode( seed.split(' ') )
143     if not seed:
144         show_message("no seed")
145         return False
146         
147     return seed
148
149
150
151 def run_settings_dialog(self):
152
153     message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
154         
155     dialog = Gtk.MessageDialog(
156         parent = self.window,
157         flags = Gtk.DialogFlags.MODAL, 
158         buttons = Gtk.ButtonsType.OK_CANCEL,
159         message_format = message)
160
161     image = Gtk.Image()
162     image.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.DIALOG)
163     image.show()
164     dialog.set_image(image)
165     dialog.set_title("Settings")
166
167     vbox = dialog.vbox
168     dialog.set_default_response(Gtk.ResponseType.OK)
169
170     fee = Gtk.HBox()
171     fee_entry = Gtk.Entry()
172     fee_label = Gtk.Label(label='Transaction fee:')
173     fee_label.set_size_request(150,10)
174     fee_label.show()
175     fee.pack_start(fee_label,False, False, 10)
176     fee_entry.set_text( str( Decimal(self.wallet.fee) /100000000 ) )
177     fee_entry.connect('changed', numbify, False)
178     fee_entry.show()
179     fee.pack_start(fee_entry,False,False, 10)
180     add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001')
181     fee.show()
182     vbox.pack_start(fee, False,False, 5)
183             
184     nz = Gtk.HBox()
185     nz_entry = Gtk.Entry()
186     nz_label = Gtk.Label(label='Display zeros:')
187     nz_label.set_size_request(150,10)
188     nz_label.show()
189     nz.pack_start(nz_label,False, False, 10)
190     nz_entry.set_text( str( self.num_zeros ))
191     nz_entry.connect('changed', numbify, True)
192     nz_entry.show()
193     nz.pack_start(nz_entry,False,False, 10)
194     add_help_button(nz, "Number of zeros displayed after the decimal point.\nFor example, if this number is 2, then '5.' is displayed as '5.00'")
195     nz.show()
196     vbox.pack_start(nz, False,False, 5)
197             
198     dialog.show()
199     r = dialog.run()
200     fee = fee_entry.get_text()
201     nz = nz_entry.get_text()
202         
203     dialog.destroy()
204     if r==Gtk.ResponseType.CANCEL:
205         return
206
207     try:
208         fee = int( 100000000 * Decimal(fee) )
209     except Exception:
210         show_message("error")
211         return
212     self.wallet.set_fee(fee)
213
214     try:
215         nz = int( nz )
216         if nz>8: nz = 8
217     except Exception:
218         show_message("error")
219         return
220
221     if self.num_zeros != nz:
222         self.num_zeros = nz
223         self.config.set_key('num_zeros',nz,True)
224         self.update_history_tab()
225
226
227
228
229 def run_network_dialog( network, parent ):
230     image = Gtk.Image()
231     image.set_from_stock(Gtk.STOCK_NETWORK, Gtk.IconSize.DIALOG)
232     if parent:
233         if network.is_connected():
234             interface = network.interface
235             status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, network.blockchain.height())
236         else:
237             status = "Not connected"
238     else:
239         import random
240         status = "Please choose a server.\nSelect cancel if you are offline."
241
242     if network.is_connected():
243         server = interface.server
244         host, port, protocol = server.split(':')
245
246     servers = network.get_servers()
247
248     dialog = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
249                                     Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, status)
250     dialog.set_title("Server")
251     dialog.set_image(image)
252     image.show()
253     
254     vbox = dialog.vbox
255     host_box = Gtk.HBox()
256     host_label = Gtk.Label(label='Connect to:')
257     host_label.set_size_request(100,-1)
258     host_label.show()
259     host_box.pack_start(host_label, False, False, 10)
260     host_entry = Gtk.Entry()
261     host_entry.set_size_request(200,-1)
262     if network.is_connected():
263         host_entry.set_text(server)
264     else:
265         host_entry.set_text("Not Connected")
266     host_entry.show()
267     host_box.pack_start(host_entry, False, False, 10)
268     add_help_button(host_box, 'The name, port number and protocol of your Electrum server, separated by a colon. Example: "ecdsa.org:50002:s". Some servers allow you to connect through http (port 80) or https (port 443)')
269     host_box.show()
270
271     p_box = Gtk.HBox(False, 10)
272     p_box.show()
273
274     p_label = Gtk.Label(label='Protocol:')
275     p_label.set_size_request(100,-1)
276     p_label.show()
277     p_box.pack_start(p_label, False, False, 10)
278
279     combobox = Gtk.ComboBoxText()
280     combobox.show()
281     combobox.append_text("TCP")
282     combobox.append_text("SSL")
283     combobox.append_text("HTTP")
284     combobox.append_text("HTTPS")
285
286     p_box.pack_start(combobox, True, True, 0)
287
288     def current_line():
289         return unicode(host_entry.get_text()).split(':')
290
291     def set_combobox(protocol):
292         combobox.set_active('tshg'.index(protocol))
293
294     def set_protocol(protocol):
295         host = current_line()[0]
296         pp = servers[host]
297         if protocol not in pp.keys():
298             protocol = pp.keys()[0]
299             set_combobox(protocol)
300         port = pp[protocol]
301         host_entry.set_text( host + ':' + port + ':' + protocol)
302
303     combobox.connect("changed", lambda x:set_protocol('tshg'[combobox.get_active()]))
304     if network.is_connected():
305         set_combobox(protocol)
306         
307     server_list = Gtk.ListStore(str)
308     for host in servers.keys():
309         server_list.append([host])
310     
311     treeview = Gtk.TreeView(model=server_list)
312     treeview.show()
313
314     label = 'Active Servers' if network.irc_servers else 'Default Servers'
315     tvcolumn = Gtk.TreeViewColumn(label)
316     treeview.append_column(tvcolumn)
317     cell = Gtk.CellRendererText()
318     tvcolumn.pack_start(cell, False)
319     tvcolumn.add_attribute(cell, 'text', 0)
320
321     vbox.pack_start(host_box, False,False, 5)
322     vbox.pack_start(p_box, True, True, 0)
323
324     #scroll = Gtk.ScrolledWindow()
325     #scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
326     #scroll.add_with_viewport(treeview)
327     #scroll.show()
328     #vbox.pack_start(scroll, True)
329     vbox.pack_start(treeview, True, True, 0)
330
331     def my_treeview_cb(treeview):
332         path, view_column = treeview.get_cursor()
333         host = server_list.get_value( server_list.get_iter(path), 0)
334
335         pp = servers[host]
336         if 't' in pp.keys():
337             protocol = 't'
338         else:
339             protocol = pp.keys()[0]
340         port = pp[protocol]
341         host_entry.set_text( host + ':' + port + ':' + protocol)
342         set_combobox(protocol)
343
344     treeview.connect('cursor-changed', my_treeview_cb)
345
346     dialog.show_all()
347     r = dialog.run()
348     server = host_entry.get_text()
349     dialog.destroy()
350
351     if r==Gtk.ResponseType.CANCEL:
352         return False
353
354     try:
355         host, port, protocol = server.split(':')
356         proxy = network.config.get('proxy')
357         auto_connect = network.config.get('auto_cycle')
358         network.set_parameters(host, port, protocol, proxy, auto_connect)
359     except Exception:
360         show_message("error:" + server)
361         return False
362
363
364
365
366
367 def show_message(message, parent=None):
368     dialog = Gtk.MessageDialog(
369         parent = parent,
370         flags = Gtk.DialogFlags.MODAL, 
371         buttons = Gtk.ButtonsType.CLOSE, 
372         message_format = message )
373     dialog.show()
374     dialog.run()
375     dialog.destroy()
376
377 def password_line(label):
378     password = Gtk.HBox()
379     password_label = Gtk.Label(label=label)
380     password_label.set_size_request(120,10)
381     password_label.show()
382     password.pack_start(password_label,False, False, 10)
383     password_entry = Gtk.Entry()
384     password_entry.set_size_request(300,-1)
385     password_entry.set_visibility(False)
386     password_entry.show()
387     password.pack_start(password_entry,False,False, 10)
388     password.show()
389     return password, password_entry
390
391 def password_dialog(parent):
392     dialog = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
393                                 Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL,  "Please enter your password.")
394     dialog.get_image().set_visible(False)
395     current_pw, current_pw_entry = password_line('Password:')
396     current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, Gtk.ResponseType.OK)
397     dialog.vbox.pack_start(current_pw, False, True, 0)
398     dialog.show()
399     result = dialog.run()
400     pw = current_pw_entry.get_text()
401     dialog.destroy()
402     if result != Gtk.ResponseType.CANCEL: return pw
403
404
405 def change_password_dialog(is_encrypted, parent):
406
407     if parent:
408         msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if is_encrypted else 'Your wallet keys are not encrypted'
409     else:
410         msg = "Please choose a password to encrypt your wallet keys"
411
412     dialog = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, msg)
413     dialog.set_title("Change password")
414     image = Gtk.Image()
415     image.set_from_stock(Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.DIALOG)
416     image.show()
417     dialog.set_image(image)
418
419     if is_encrypted:
420         current_pw, current_pw_entry = password_line('Current password:')
421         dialog.vbox.pack_start(current_pw, False, True, 0)
422
423     password, password_entry = password_line('New password:')
424     dialog.vbox.pack_start(password, False, True, 5)
425     password2, password2_entry = password_line('Confirm password:')
426     dialog.vbox.pack_start(password2, False, True, 5)
427
428     dialog.show()
429     result = dialog.run()
430     password = current_pw_entry.get_text() if is_encrypted else None
431     new_password = password_entry.get_text()
432     new_password2 = password2_entry.get_text()
433     dialog.destroy()
434     if result == Gtk.ResponseType.CANCEL: 
435         return
436
437     if new_password != new_password2:
438         show_message("passwords do not match")
439         return change_password_dialog(is_encrypted, parent)
440
441     if not new_password:
442         new_password = None
443
444     return True, password, new_password
445
446
447
448 def add_help_button(hbox, message):
449     button = Gtk.Button('?')
450     button.connect("clicked", lambda x: show_message(message))
451     button.show()
452     hbox.pack_start(button,False, False, 0)
453
454
455 class MyWindow(Gtk.Window): __gsignals__ = dict( mykeypress = (GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, None, (str,)) )
456
457 GObject.type_register(MyWindow)
458 #FIXME: can't find docs how to create keybindings in PyGI
459 #Gtk.binding_entry_add_signall(MyWindow, Gdk.KEY_W, Gdk.ModifierType.CONTROL_MASK, 'mykeypress', ['ctrl+W'])
460 #Gtk.binding_entry_add_signall(MyWindow, Gdk.KEY_Q, Gdk.ModifierType.CONTROL_MASK, 'mykeypress', ['ctrl+Q'])
461
462
463 class ElectrumWindow:
464
465     def show_message(self, msg):
466         show_message(msg, self.window)
467
468     def __init__(self, wallet, config, network):
469         self.config = config
470         self.wallet = wallet
471         self.network = network
472         self.funds_error = False # True if not enough funds
473         self.num_zeros = int(self.config.get('num_zeros',0))
474
475         self.window = MyWindow(Gtk.WindowType.TOPLEVEL)
476         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
477         if not self.wallet.seed: title += ' [seedless]'
478         self.window.set_title(title)
479         self.window.connect("destroy", Gtk.main_quit)
480         self.window.set_border_width(0)
481         self.window.connect('mykeypress', Gtk.main_quit)
482         self.window.set_default_size(720, 350)
483         self.wallet_updated = False
484
485         vbox = Gtk.VBox()
486
487         self.notebook = Gtk.Notebook()
488         self.create_history_tab()
489         if self.wallet.seed:
490             self.create_send_tab()
491         self.create_recv_tab()
492         self.create_book_tab()
493         self.create_about_tab()
494         self.notebook.show()
495         vbox.pack_start(self.notebook, True, True, 2)
496         
497         self.status_bar = Gtk.Statusbar()
498         vbox.pack_start(self.status_bar, False, False, 0)
499
500         self.status_image = Gtk.Image()
501         self.status_image.set_from_stock(Gtk.STOCK_NO, Gtk.IconSize.MENU)
502         self.status_image.set_alignment(True, 0.5  )
503         self.status_image.show()
504
505         self.network_button = Gtk.Button()
506         self.network_button.connect("clicked", lambda x: run_network_dialog(self.network, self.window) )
507         self.network_button.add(self.status_image)
508         self.network_button.set_relief(Gtk.ReliefStyle.NONE)
509         self.network_button.show()
510         self.status_bar.pack_end(self.network_button, False, False, 0)
511
512         if self.wallet.seed:
513             def seedb(w, wallet):
514                 if wallet.use_encryption:
515                     password = password_dialog(self.window)
516                     if not password: return
517                 else: password = None
518                 show_seed_dialog(wallet, password, self.window)
519             button = Gtk.Button('S')
520             button.connect("clicked", seedb, self.wallet )
521             button.set_relief(Gtk.ReliefStyle.NONE)
522             button.show()
523             self.status_bar.pack_end(button,False, False, 0)
524
525         settings_icon = Gtk.Image()
526         settings_icon.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU)
527         settings_icon.set_alignment(0.5, 0.5)
528         settings_icon.set_size_request(16,16 )
529         settings_icon.show()
530
531         prefs_button = Gtk.Button()
532         prefs_button.connect("clicked", lambda x: run_settings_dialog(self) )
533         prefs_button.add(settings_icon)
534         prefs_button.set_tooltip_text("Settings")
535         prefs_button.set_relief(Gtk.ReliefStyle.NONE)
536         prefs_button.show()
537         self.status_bar.pack_end(prefs_button,False,False, 0)
538
539         self.pw_icon = Gtk.Image()
540         self.pw_icon.set_from_stock(Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.MENU)
541         self.pw_icon.set_alignment(0.5, 0.5)
542         self.pw_icon.set_size_request(16,16 )
543         self.pw_icon.show()
544
545         if self.wallet.seed:
546
547             if self.wallet.use_encryption:
548                 self.pw_icon.set_tooltip_text('Wallet is encrypted')
549             else:
550                 self.pw_icon.set_tooltip_text('Wallet is unencrypted')
551
552             password_button = Gtk.Button()
553             password_button.connect("clicked", self.do_update_password, self.wallet)
554             password_button.add(self.pw_icon)
555             password_button.set_relief(Gtk.ReliefStyle.NONE)
556             password_button.show()
557             self.status_bar.pack_end(password_button,False,False, 0)
558
559         self.window.add(vbox)
560         self.window.show_all()
561         #self.fee_box.hide()
562
563         self.context_id = self.status_bar.get_context_id("statusbar")
564         self.update_status_bar()
565
566         self.network.register_callback('updated', self.update_callback)
567
568
569         def update_status_bar_thread():
570             while True:
571                 GObject.idle_add( self.update_status_bar )
572                 time.sleep(0.5)
573
574
575         def check_recipient_thread():
576             old_r = ''
577             while True:
578                 time.sleep(0.5)
579                 if self.payto_entry.is_focus():
580                     continue
581                 r = self.payto_entry.get_text()
582                 if r != old_r:
583                     old_r = r
584                     r = r.strip()
585                     if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
586                         try:
587                             to_address = self.wallet.get_alias(r, interactive=False)
588                         except Exception:
589                             continue
590                         if to_address:
591                             s = r + ' <' + to_address + '>'
592                             GObject.idle_add( lambda: self.payto_entry.set_text(s) )
593                 
594
595         thread.start_new_thread(update_status_bar_thread, ())
596         if self.wallet.seed:
597             thread.start_new_thread(check_recipient_thread, ())
598         self.notebook.set_current_page(0)
599
600     def update_callback(self):
601         self.wallet_updated = True
602
603     def do_update_password(self, button, wallet):
604         if not wallet.seed:
605             show_message("No seed")
606             return
607
608         res = change_password_dialog(wallet.use_encryption, self.window)
609         if res:
610             _, password, new_password = res
611
612             try:
613                 wallet.get_seed(password)
614             except Exception:
615                 show_message("Incorrect password")
616                 return
617
618             wallet.update_password(password, new_password)
619
620             if wallet.use_encryption:
621                 self.pw_icon.set_tooltip_text('Wallet is encrypted')
622             else:
623                 self.pw_icon.set_tooltip_text('Wallet is unencrypted')
624
625
626     def add_tab(self, page, name):
627         tab_label = Gtk.Label(label=name)
628         tab_label.show()
629         self.notebook.append_page(page, tab_label)
630
631
632     def create_send_tab(self):
633         
634         page = vbox = Gtk.VBox()
635         page.show()
636
637         payto = Gtk.HBox()
638         payto_label = Gtk.Label(label='Pay to:')
639         payto_label.set_size_request(100,-1)
640         payto.pack_start(payto_label, False, False, 0)
641         payto_entry = Gtk.Entry()
642         payto_entry.set_size_request(450, 26)
643         payto.pack_start(payto_entry, False, False, 0)
644         vbox.pack_start(payto, False, False, 5)
645
646         message = Gtk.HBox()
647         message_label = Gtk.Label(label='Description:')
648         message_label.set_size_request(100,-1)
649         message.pack_start(message_label, False, False, 0)
650         message_entry = Gtk.Entry()
651         message_entry.set_size_request(450, 26)
652         message.pack_start(message_entry, False, False, 0)
653         vbox.pack_start(message, False, False, 5)
654
655         amount_box = Gtk.HBox()
656         amount_label = Gtk.Label(label='Amount:')
657         amount_label.set_size_request(100,-1)
658         amount_box.pack_start(amount_label, False, False, 0)
659         amount_entry = Gtk.Entry()
660         amount_entry.set_size_request(120, -1)
661         amount_box.pack_start(amount_entry, False, False, 0)
662         vbox.pack_start(amount_box, False, False, 5)
663
664         self.fee_box = fee_box = Gtk.HBox()
665         fee_label = Gtk.Label(label='Fee:')
666         fee_label.set_size_request(100,-1)
667         fee_box.pack_start(fee_label, False, False, 0)
668         fee_entry = Gtk.Entry()
669         fee_entry.set_size_request(60, 26)
670         fee_box.pack_start(fee_entry, False, False, 0)
671         vbox.pack_start(fee_box, False, False, 5)
672
673         end_box = Gtk.HBox()
674         empty_label = Gtk.Label(label='')
675         empty_label.set_size_request(100,-1)
676         end_box.pack_start(empty_label, False, False, 0)
677         send_button = Gtk.Button("Send")
678         send_button.show()
679         end_box.pack_start(send_button, False, False, 0)
680         clear_button = Gtk.Button("Clear")
681         clear_button.show()
682         end_box.pack_start(clear_button, False, False, 15)
683         send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
684         clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
685
686         vbox.pack_start(end_box, False, False, 5)
687
688         # display this line only if there is a signature
689         payto_sig = Gtk.HBox()
690         payto_sig_id = Gtk.Label(label='')
691         payto_sig.pack_start(payto_sig_id, False, False, 0)
692         vbox.pack_start(payto_sig, True, True, 5)
693         
694
695         self.user_fee = False
696
697         def entry_changed( entry, is_fee ):
698             self.funds_error = False
699             amount = numbify(amount_entry)
700             fee = numbify(fee_entry)
701             if not is_fee: fee = None
702             if amount is None:
703                 return
704             #assume two outputs - one for change
705             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, 2 )
706             if not is_fee:
707                 fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
708                 self.fee_box.show()
709             if inputs:
710                 amount_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#000000"))
711                 fee_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#000000"))
712                 send_button.set_sensitive(True)
713             else:
714                 send_button.set_sensitive(False)
715                 amount_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#cc0000"))
716                 fee_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#cc0000"))
717                 self.funds_error = True
718
719         amount_entry.connect('changed', entry_changed, False)
720         fee_entry.connect('changed', entry_changed, True)        
721
722         self.payto_entry = payto_entry
723         self.payto_fee_entry = fee_entry
724         self.payto_sig_id = payto_sig_id
725         self.payto_sig = payto_sig
726         self.amount_entry = amount_entry
727         self.message_entry = message_entry
728         self.add_tab(page, 'Send')
729
730     def set_frozen(self,entry,frozen):
731         if frozen:
732             entry.set_editable(False)
733             entry.set_has_frame(False)
734             entry.modify_base(Gtk.StateType.NORMAL, Gdk.color_parse("#eeeeee"))
735         else:
736             entry.set_editable(True)
737             entry.set_has_frame(True)
738             entry.modify_base(Gtk.StateType.NORMAL, Gdk.color_parse("#ffffff"))
739
740     def set_url(self, url):
741         payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
742         self.notebook.set_current_page(1)
743         self.payto_entry.set_text(payto)
744         self.message_entry.set_text(message)
745         self.amount_entry.set_text(amount)
746         if identity:
747             self.set_frozen(self.payto_entry,True)
748             self.set_frozen(self.amount_entry,True)
749             self.set_frozen(self.message_entry,True)
750             self.payto_sig_id.set_text( '      The bitcoin URI was signed by ' + identity )
751         else:
752             self.payto_sig.set_visible(False)
753
754     def create_about_tab(self):
755         from gi.repository import Pango
756         page = Gtk.VBox()
757         page.show()
758         tv = Gtk.TextView()
759         tv.set_editable(False)
760         tv.set_cursor_visible(False)
761         tv.modify_font(Pango.FontDescription(MONOSPACE_FONT))
762         scroll = Gtk.ScrolledWindow()
763         scroll.add(tv)
764         page.pack_start(scroll, True, True, 0)
765         self.info = tv.get_buffer()
766         self.add_tab(page, 'Wall')
767
768     def do_clear(self, w, data):
769         self.payto_sig.set_visible(False)
770         self.payto_fee_entry.set_text('')
771         for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
772             self.set_frozen(entry,False)
773             entry.set_text('')
774
775     def question(self,msg):
776         dialog = Gtk.MessageDialog( self.window, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, msg)
777         dialog.show()
778         result = dialog.run()
779         dialog.destroy()
780         return result == Gtk.ResponseType.OK
781
782     def do_send(self, w, data):
783         payto_entry, label_entry, amount_entry, fee_entry = data
784         label = label_entry.get_text()
785         r = payto_entry.get_text()
786         r = r.strip()
787
788         m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
789         m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
790         
791         if m1:
792             to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
793             if not to_address:
794                 return
795             else:
796                 self.update_sending_tab()
797
798         elif m2:
799             to_address = m2.group(5)
800         else:
801             to_address = r
802
803         if not is_valid(to_address):
804             self.show_message( "invalid bitcoin address:\n"+to_address)
805             return
806
807         try:
808             amount = int( Decimal(amount_entry.get_text()) * 100000000 )
809         except Exception:
810             self.show_message( "invalid amount")
811             return
812         try:
813             fee = int( Decimal(fee_entry.get_text()) * 100000000 )
814         except Exception:
815             self.show_message( "invalid fee")
816             return
817
818         if self.wallet.use_encryption:
819             password = password_dialog(self.window)
820             if not password:
821                 return
822         else:
823             password = None
824
825         try:
826             tx = self.wallet.mktx( [(to_address, amount)], password, fee )
827         except Exception as e:
828             self.show_message(str(e))
829             return
830
831         if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
832             self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." )
833             return
834
835             
836         if label: 
837             self.wallet.labels[tx.hash()] = label
838
839         status, msg = self.wallet.sendtx( tx )
840         if status:
841             self.show_message( "payment sent.\n" + msg )
842             payto_entry.set_text("")
843             label_entry.set_text("")
844             amount_entry.set_text("")
845             fee_entry.set_text("")
846             #self.fee_box.hide()
847             self.update_sending_tab()
848         else:
849             self.show_message( msg )
850
851
852     def treeview_button_press(self, treeview, event):
853         if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
854             c = treeview.get_cursor()[0]
855             if treeview == self.history_treeview:
856                 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
857                 self.show_message(tx_details)
858             elif treeview == self.contacts_treeview:
859                 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
860                 #a = self.wallet.aliases.get(m)
861                 #if a:
862                 #    if a[0] in self.wallet.authorities.keys():
863                 #        s = self.wallet.authorities.get(a[0])
864                 #    else:
865                 #        s = "self-signed"
866                 #    msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
867                 #    self.show_message(msg)
868             
869
870     def treeview_key_press(self, treeview, event):
871         c = treeview.get_cursor()[0]
872         if event.keyval == Gdk.KEY_Up:
873             if c and c[0] == 0:
874                 treeview.parent.grab_focus()
875                 treeview.set_cursor((0,))
876         elif event.keyval == Gdk.KEY_Return:
877             if treeview == self.history_treeview:
878                 tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
879                 self.show_message(tx_details)
880             elif treeview == self.contacts_treeview:
881                 m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
882                 #a = self.wallet.aliases.get(m)
883                 #if a:
884                 #    if a[0] in self.wallet.authorities.keys():
885                 #        s = self.wallet.authorities.get(a[0])
886                 #    else:
887                 #        s = "self"
888                 #    msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
889                 #    self.show_message(msg)
890
891         return False
892
893     def create_history_tab(self):
894
895         self.history_list = Gtk.ListStore(str, str, str, str, 'gboolean',  str, str, str, str)
896         treeview = Gtk.TreeView(model=self.history_list)
897         self.history_treeview = treeview
898         treeview.set_tooltip_column(7)
899         treeview.show()
900         treeview.connect('key-press-event', self.treeview_key_press)
901         treeview.connect('button-press-event', self.treeview_button_press)
902
903         tvcolumn = Gtk.TreeViewColumn('')
904         treeview.append_column(tvcolumn)
905         cell = Gtk.CellRendererPixbuf()
906         tvcolumn.pack_start(cell, False)
907         tvcolumn.set_attributes(cell, stock_id=1)
908
909         tvcolumn = Gtk.TreeViewColumn('Date')
910         treeview.append_column(tvcolumn)
911         cell = Gtk.CellRendererText()
912         tvcolumn.pack_start(cell, False)
913         tvcolumn.add_attribute(cell, 'text', 2)
914
915         tvcolumn = Gtk.TreeViewColumn('Description')
916         treeview.append_column(tvcolumn)
917         cell = Gtk.CellRendererText()
918         cell.set_property('foreground', 'grey')
919         cell.set_property('family', MONOSPACE_FONT)
920         cell.set_property('editable', True)
921         def edited_cb(cell, path, new_text, h_list):
922             tx = h_list.get_value( h_list.get_iter(path), 0)
923             self.wallet.set_label(tx,new_text)
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.PolicyType.NEVER, Gtk.PolicyType.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, 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.set_label(address, new_text)
999             self.update_receiving_tab()
1000             self.update_sending_tab()
1001             self.update_history_tab()
1002         cell.connect('edited', edited_cb2, liststore)
1003         tvcolumn.pack_start(cell, True)
1004         tvcolumn.add_attribute(cell, 'text', 1)
1005
1006         tvcolumn = Gtk.TreeViewColumn('Tx')
1007         treeview.append_column(tvcolumn)
1008         cell = Gtk.CellRendererText()
1009         tvcolumn.pack_start(cell, True)
1010         tvcolumn.add_attribute(cell, 'text', 2)
1011
1012         if is_recv:
1013             tvcolumn = Gtk.TreeViewColumn('Balance')
1014             treeview.append_column(tvcolumn)
1015             cell = Gtk.CellRendererText()
1016             tvcolumn.pack_start(cell, True)
1017             tvcolumn.add_attribute(cell, 'text', 3)
1018             tvcolumn = Gtk.TreeViewColumn('Type')
1019             treeview.append_column(tvcolumn)
1020             cell = Gtk.CellRendererText()
1021             tvcolumn.pack_start(cell, True)
1022             tvcolumn.add_attribute(cell, 'text', 4)
1023
1024         scroll = Gtk.ScrolledWindow()
1025         scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
1026         scroll.add(treeview)
1027
1028         hbox = Gtk.HBox()
1029         if not is_recv:
1030             button = Gtk.Button("New")
1031             button.connect("clicked", self.newaddress_dialog)
1032             button.show()
1033             hbox.pack_start(button,False, False, 0)
1034
1035         def showqrcode(w, treeview, liststore):
1036             path, col = treeview.get_cursor()
1037             if not path: return
1038             address = liststore.get_value(liststore.get_iter(path), 0)
1039             qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1040             qr.addData(address)
1041             qr.make()
1042             boxsize = 7
1043             boxcount_row = qr.getModuleCount()
1044             size = (boxcount_row + 4) * boxsize
1045             def area_expose_cb(area, cr):
1046                 style = area.get_style()
1047                 Gdk.cairo_set_source_color(cr, style.white)
1048                 cr.rectangle(0, 0, size, size)
1049                 cr.fill()
1050                 Gdk.cairo_set_source_color(cr, style.black)
1051                 for r in range(boxcount_row):
1052                     for c in range(boxcount_row):
1053                         if qr.isDark(r, c):
1054                             cr.rectangle((c + 2) * boxsize, (r + 2) * boxsize, boxsize, boxsize)
1055                             cr.fill()
1056             area = Gtk.DrawingArea()
1057             area.set_size_request(size, size)
1058             area.connect("draw", area_expose_cb)
1059             area.show()
1060             dialog = Gtk.Dialog(address, parent=self.window, flags=Gtk.DialogFlags.MODAL, buttons = ("ok",1))
1061             dialog.vbox.add(area)
1062             dialog.run()
1063             dialog.destroy()
1064
1065         button = Gtk.Button("QR")
1066         button.connect("clicked", showqrcode, treeview, liststore)
1067         button.show()
1068         hbox.pack_start(button,False, False, 0)
1069
1070         button = Gtk.Button("Copy to clipboard")
1071         def copy2clipboard(w, treeview, liststore):
1072             import platform
1073             path, col =  treeview.get_cursor()
1074             if path:
1075                 address =  liststore.get_value( liststore.get_iter(path), 0)
1076                 if platform.system() == 'Windows':
1077                     from Tkinter import Tk
1078                     r = Tk()
1079                     r.withdraw()
1080                     r.clipboard_clear()
1081                     r.clipboard_append( address )
1082                     r.destroy()
1083                 else:
1084                     atom = Gdk.atom_intern('CLIPBOARD', True)
1085                     c = Gtk.Clipboard.get(atom)
1086                     c.set_text( address, len(address) )
1087         button.connect("clicked", copy2clipboard, treeview, liststore)
1088         button.show()
1089         hbox.pack_start(button,False, False, 0)
1090
1091         if is_recv:
1092             button = Gtk.Button("Freeze")
1093             def freeze_address(w, treeview, liststore, wallet):
1094                 path, col = treeview.get_cursor()
1095                 if path:
1096                     address = liststore.get_value( liststore.get_iter(path), 0)
1097                     if address in wallet.frozen_addresses:
1098                         wallet.unfreeze(address)
1099                     else:
1100                         wallet.freeze(address)
1101                     self.update_receiving_tab()
1102             button.connect("clicked", freeze_address, treeview, liststore, self.wallet)
1103             button.show()
1104             hbox.pack_start(button,False, False, 0)
1105
1106         if not is_recv:
1107             button = Gtk.Button("Pay to")
1108             def payto(w, treeview, liststore):
1109                 path, col =  treeview.get_cursor()
1110                 if path:
1111                     address =  liststore.get_value( liststore.get_iter(path), 0)
1112                     self.payto_entry.set_text( address )
1113                     self.notebook.set_current_page(1)
1114                     self.amount_entry.grab_focus()
1115
1116             button.connect("clicked", payto, treeview, liststore)
1117             button.show()
1118             hbox.pack_start(button,False, False, 0)
1119
1120         vbox = Gtk.VBox()
1121         vbox.pack_start(scroll,True, True, 0)
1122         vbox.pack_start(hbox, False, False, 0)
1123         return vbox
1124
1125     def update_status_bar(self):
1126         interface = self.network.interface
1127         if self.funds_error:
1128             text = "Not enough funds"
1129         elif interface and interface.is_connected:
1130             self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height()))
1131             if not self.wallet.up_to_date:
1132                 self.status_image.set_from_stock(Gtk.STOCK_REFRESH, Gtk.IconSize.MENU)
1133                 text = "Synchronizing..."
1134             else:
1135                 self.status_image.set_from_stock(Gtk.STOCK_YES, Gtk.IconSize.MENU)
1136                 self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height()))
1137                 c, u = self.wallet.get_balance()
1138                 text =  "Balance: %s "%( format_satoshis(c,False,self.num_zeros) )
1139                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.num_zeros).strip() )
1140         else:
1141             self.status_image.set_from_stock(Gtk.STOCK_NO, Gtk.IconSize.MENU)
1142             self.network_button.set_tooltip_text("Not connected.")
1143             text = "Not connected"
1144
1145         self.status_bar.pop(self.context_id) 
1146         self.status_bar.push(self.context_id, text)
1147
1148         if self.wallet.up_to_date and self.wallet_updated:
1149             self.update_history_tab()
1150             self.update_receiving_tab()
1151             # addressbook too...
1152             self.info.set_text( self.network.banner )
1153             self.wallet_updated = False
1154
1155     def update_receiving_tab(self):
1156         self.recv_list.clear()
1157         for address in self.wallet.addresses(True):
1158             Type = "R"
1159             c = u = 0
1160             if self.wallet.is_change(address): Type = "C"
1161             if address in self.wallet.imported_keys.keys():
1162                 Type = "I"
1163             c, u = self.wallet.get_addr_balance(address)
1164             if address in self.wallet.frozen_addresses: Type = Type + "F"
1165             label = self.wallet.labels.get(address)
1166             h = self.wallet.history.get(address,[])
1167             n = len(h)
1168             tx = "0" if n==0 else "%d"%n
1169             self.recv_list.append((address, label, tx, format_satoshis(c,False,self.num_zeros), Type ))
1170
1171     def update_sending_tab(self):
1172         # detect addresses that are not mine in history, add them here...
1173         self.addressbook_list.clear()
1174         #for alias, v in self.wallet.aliases.items():
1175         #    s, target = v
1176         #    label = self.wallet.labels.get(alias)
1177         #    self.addressbook_list.append((alias, label, '-'))
1178             
1179         for address in self.wallet.addressbook:
1180             label = self.wallet.labels.get(address)
1181             n = self.wallet.get_num_tx(address)
1182             self.addressbook_list.append((address, label, "%d"%n))
1183
1184     def update_history_tab(self):
1185         cursor = self.history_treeview.get_cursor()[0]
1186         self.history_list.clear()
1187
1188         for item in self.wallet.get_tx_history():
1189             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1190             if conf > 0:
1191                 try:
1192                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1193                 except Exception:
1194                     time_str = "------"
1195                 conf_icon = Gtk.STOCK_APPLY
1196             elif conf == -1:
1197                 time_str = 'unverified'
1198                 conf_icon = None
1199             else:
1200                 time_str = 'pending'
1201                 conf_icon = Gtk.STOCK_EXECUTE
1202
1203             label, is_default_label = self.wallet.get_label(tx_hash)
1204             tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1205             details = self.get_tx_details(tx_hash)
1206
1207             self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1208                                         format_satoshis(value,True,self.num_zeros, whitespaces=True),
1209                                         format_satoshis(balance,False,self.num_zeros, whitespaces=True), tooltip, details] )
1210         if cursor: self.history_treeview.set_cursor( cursor )
1211
1212
1213     def get_tx_details(self, tx_hash):
1214         import datetime
1215         if not tx_hash: return ''
1216         tx = self.wallet.transactions.get(tx_hash)
1217         is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
1218         conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1219
1220         if timestamp:
1221             time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1222         else:
1223             time_str = 'pending'
1224
1225         inputs = map(lambda x: x.get('address'), tx.inputs)
1226         outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1227         tx_details = "Transaction Details" +"\n\n" \
1228             + "Transaction ID:\n" + tx_hash + "\n\n" \
1229             + "Status: %d confirmations\n"%conf
1230         if is_mine:
1231             if fee: 
1232                 tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1233                               + "Transaction fee: %s\n"% format_satoshis(fee, False)
1234             else:
1235                 tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1236                               + "Transaction fee: unknown\n"
1237         else:
1238             tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1239
1240         tx_details += "Date: %s\n\n"%time_str \
1241             + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1242             + "Outputs:\n-"+ '\n-'.join(outputs)
1243
1244         return tx_details
1245
1246
1247
1248     def newaddress_dialog(self, w):
1249
1250         title = "New Contact" 
1251         dialog = Gtk.Dialog(title, parent=self.window, 
1252                             flags=Gtk.DialogFlags.MODAL,
1253                             buttons= ("cancel", 0, "ok",1)  )
1254         dialog.show()
1255
1256         label = Gtk.HBox()
1257         label_label = Gtk.Label(label='Label:')
1258         label_label.set_size_request(120,10)
1259         label_label.show()
1260         label.pack_start(label_label, True, True, 0)
1261         label_entry = Gtk.Entry()
1262         label_entry.show()
1263         label.pack_start(label_entry, True, True, 0)
1264         label.show()
1265         dialog.vbox.pack_start(label, False, True, 5)
1266
1267         address = Gtk.HBox()
1268         address_label = Gtk.Label(label='Address:')
1269         address_label.set_size_request(120,10)
1270         address_label.show()
1271         address.pack_start(address_label, True, True, 0)
1272         address_entry = Gtk.Entry()
1273         address_entry.show()
1274         address.pack_start(address_entry, True, True, 0)
1275         address.show()
1276         dialog.vbox.pack_start(address, False, True, 5)
1277         
1278         result = dialog.run()
1279         address = address_entry.get_text()
1280         label = label_entry.get_text()
1281         dialog.destroy()
1282
1283         if result == 1:
1284             if is_valid(address):
1285                 self.wallet.add_contact(address,label)
1286                 self.update_sending_tab()
1287             else:
1288                 errorDialog = Gtk.MessageDialog(
1289                     parent=self.window,
1290                     flags=Gtk.DialogFlags.MODAL, 
1291                     buttons= Gtk.ButtonsType.CLOSE, 
1292                     message_format = "Invalid address")
1293                 errorDialog.show()
1294                 errorDialog.run()
1295                 errorDialog.destroy()
1296
1297     
1298
1299 class ElectrumGui():
1300
1301     def __init__(self, config, network):
1302         self.network = network
1303         self.config = config
1304
1305
1306     def main(self, url=None):
1307
1308         storage = WalletStorage(self.config)
1309         if not storage.file_exists:
1310             action = self.restore_or_create()
1311             if not action:
1312                 exit()
1313             self.wallet = wallet = Wallet(storage)
1314             gap = self.config.get('gap_limit', 5)
1315             if gap != 5:
1316                 wallet.gap_limit = gap
1317                 wallet.storage.put('gap_limit', gap, True)
1318
1319
1320             if action == 'create':
1321                 wallet.init_seed(None)
1322                 show_seed_dialog(wallet, None, None)
1323                 r = change_password_dialog(False, None)
1324                 password = r[2] if r else None
1325                 wallet.save_seed(password)
1326                 wallet.synchronize()  # generate first addresses offline
1327
1328             elif action == 'restore':
1329                 seed = self.seed_dialog()
1330                 wallet.init_seed(seed)
1331                 r = change_password_dialog(False, None)
1332                 password = r[2] if r else None
1333                 wallet.save_seed(password)
1334                 
1335             else:
1336                 exit()
1337         else:
1338             self.wallet = Wallet(storage)
1339             action = None
1340
1341         self.wallet.start_threads(self.network)
1342
1343         if action == 'restore':
1344             self.restore_wallet(wallet)
1345
1346         w = ElectrumWindow(self.wallet, self.config, self.network)
1347         if url: w.set_url(url)
1348         Gtk.main()
1349
1350     def restore_or_create(self):
1351         return restore_create_dialog()
1352
1353     def seed_dialog(self):
1354         return run_recovery_dialog()
1355
1356     def network_dialog(self):
1357         return run_network_dialog( self.network, parent=None )
1358
1359
1360     def restore_wallet(self, wallet):
1361
1362         dialog = Gtk.MessageDialog(
1363             parent = None,
1364             flags = Gtk.DialogFlags.MODAL, 
1365             buttons = Gtk.ButtonsType.CANCEL, 
1366             message_format = "Please wait..."  )
1367         dialog.show()
1368
1369         def recover_thread( wallet, dialog ):
1370             wallet.restore(lambda x:x)
1371             GObject.idle_add( dialog.destroy )
1372
1373         thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1374         r = dialog.run()
1375         dialog.destroy()
1376         if r==Gtk.ResponseType.CANCEL: return False
1377         if not wallet.is_found():
1378             show_message("No transactions found for this seed")
1379
1380         return True