wizard: start wallet threads
[electrum-nvc.git] / gui / gui_classic.py
1 #!/usr/bin/env python
2 #
3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 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 sys, time, datetime, re, threading
20 from i18n import _, set_language
21 from electrum.util import print_error, print_msg
22 import os.path, json, ast, traceback
23 import shutil
24
25
26 try:
27     import PyQt4
28 except:
29     sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
30
31 from PyQt4.QtGui import *
32 from PyQt4.QtCore import *
33 import PyQt4.QtCore as QtCore
34
35 from electrum.bitcoin import MIN_RELAY_TX_FEE
36
37 try:
38     import icons_rc
39 except:
40     sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o gui/icons_rc.py'")
41
42 from electrum.wallet import format_satoshis
43 from electrum.bitcoin import Transaction, is_valid
44 from electrum import mnemonic
45 from electrum import util, bitcoin, commands, Interface, Wallet
46 from electrum import SimpleConfig, Wallet, WalletStorage
47
48
49 import bmp, pyqrnative
50 import exchange_rate
51
52 from amountedit import AmountEdit
53 from network_dialog import NetworkDialog
54 from qrcodewidget import QRCodeWidget
55
56 from decimal import Decimal
57
58 import platform
59 import httplib
60 import socket
61 import webbrowser
62 import csv
63
64 if platform.system() == 'Windows':
65     MONOSPACE_FONT = 'Lucida Console'
66 elif platform.system() == 'Darwin':
67     MONOSPACE_FONT = 'Monaco'
68 else:
69     MONOSPACE_FONT = 'monospace'
70
71 from electrum import ELECTRUM_VERSION
72 import re
73
74 from qt_util import *
75
76 class UpdateLabel(QLabel):
77     def __init__(self, config, parent=None):
78         QLabel.__init__(self, parent)
79         self.new_version = False
80
81         try:
82             con = httplib.HTTPConnection('electrum.org', 80, timeout=5)
83             con.request("GET", "/version")
84             res = con.getresponse()
85         except socket.error as msg:
86             print_error("Could not retrieve version information")
87             return
88             
89         if res.status == 200:
90             self.latest_version = res.read()
91             self.latest_version = self.latest_version.replace("\n","")
92             if(re.match('^\d+(\.\d+)*$', self.latest_version)):
93                 self.config = config
94                 self.current_version = ELECTRUM_VERSION
95                 if(self.compare_versions(self.latest_version, self.current_version) == 1):
96                     latest_seen = self.config.get("last_seen_version",ELECTRUM_VERSION)
97                     if(self.compare_versions(self.latest_version, latest_seen) == 1):
98                         self.new_version = True
99                         self.setText(_("New version available") + ": " + self.latest_version)
100
101
102     def compare_versions(self, version1, version2):
103         def normalize(v):
104             return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
105         return cmp(normalize(version1), normalize(version2))
106
107     def ignore_this_version(self):
108         self.setText("")
109         self.config.set_key("last_seen_version", self.latest_version, True)
110         QMessageBox.information(self, _("Preference saved"), _("Notifications about this update will not be shown again."))
111         self.dialog.done(0)
112
113     def ignore_all_version(self):
114         self.setText("")
115         self.config.set_key("last_seen_version", "9.9.9", True)
116         QMessageBox.information(self, _("Preference saved"), _("No more notifications about version updates will be shown."))
117         self.dialog.done(0)
118   
119     def open_website(self):
120         webbrowser.open("http://electrum.org/download.html")
121         self.dialog.done(0)
122
123     def mouseReleaseEvent(self, event):
124         dialog = QDialog(self)
125         dialog.setWindowTitle(_('Electrum update'))
126         dialog.setModal(1)
127
128         main_layout = QGridLayout()
129         main_layout.addWidget(QLabel(_("A new version of Electrum is available:")+" " + self.latest_version), 0,0,1,3)
130         
131         ignore_version = QPushButton(_("Ignore this version"))
132         ignore_version.clicked.connect(self.ignore_this_version)
133
134         ignore_all_versions = QPushButton(_("Ignore all versions"))
135         ignore_all_versions.clicked.connect(self.ignore_all_version)
136
137         open_website = QPushButton(_("Goto download page"))
138         open_website.clicked.connect(self.open_website)
139
140         main_layout.addWidget(ignore_version, 1, 0)
141         main_layout.addWidget(ignore_all_versions, 1, 1)
142         main_layout.addWidget(open_website, 1, 2)
143
144         dialog.setLayout(main_layout)
145
146         self.dialog = dialog
147         
148         if not dialog.exec_(): return
149
150
151
152
153 class MyTreeWidget(QTreeWidget):
154     def __init__(self, parent):
155         QTreeWidget.__init__(self, parent)
156         def ddfr(item):
157             if not item: return
158             for i in range(0,self.viewport().height()/5):
159                 if self.itemAt(QPoint(0,i*5)) == item:
160                     break
161             else:
162                 return
163             for j in range(0,30):
164                 if self.itemAt(QPoint(0,i*5 + j)) != item:
165                     break
166             self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
167
168         self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
169         
170
171
172
173 class StatusBarButton(QPushButton):
174     def __init__(self, icon, tooltip, func):
175         QPushButton.__init__(self, icon, '')
176         self.setToolTip(tooltip)
177         self.setFlat(True)
178         self.setMaximumWidth(25)
179         self.clicked.connect(func)
180         self.func = func
181
182     def keyPressEvent(self, e):
183         if e.key() == QtCore.Qt.Key_Return:
184             apply(self.func,())
185
186
187
188
189
190
191
192
193
194
195 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive":[[370], [370,200,130]] }
196
197 class ElectrumWindow(QMainWindow):
198     def changeEvent(self, event):
199         flags = self.windowFlags();
200         if event and event.type() == QtCore.QEvent.WindowStateChange:
201             if self.windowState() & QtCore.Qt.WindowMinimized:
202                 self.build_menu(True)
203                 # The only way to toggle the icon in the window managers taskbar is to use the Qt.Tooltip flag
204                 # The problem is that it somehow creates an (in)visible window that will stay active and prevent
205                 # Electrum from closing.
206                 # As for now I have no clue how to implement a proper 'hide to tray' functionality.
207                 # self.setWindowFlags(flags & ~Qt.ToolTip)
208             elif event.oldState() & QtCore.Qt.WindowMinimized:
209                 self.build_menu(False)
210                 #self.setWindowFlags(flags | Qt.ToolTip)
211
212     def build_menu(self, is_hidden = False):
213         m = QMenu()
214         if self.isMinimized():
215             m.addAction(_("Show"), self.showNormal)
216         else:
217             m.addAction(_("Hide"), self.showMinimized)
218
219         m.addSeparator()
220         m.addAction(_("Exit Electrum"), self.close)
221         self.tray.setContextMenu(m)
222
223     def tray_activated(self, reason):
224         if reason == QSystemTrayIcon.DoubleClick:
225             self.showNormal()
226
227
228     def __init__(self, config):
229         QMainWindow.__init__(self)
230
231         self.config = config
232         self.init_plugins()
233
234         self._close_electrum = False
235         self.lite = None
236         self.current_account = self.config.get("current_account", None)
237
238         self.icon = QIcon(os.getcwd() + '/icons/electrum.png')
239         self.tray = QSystemTrayIcon(self.icon, self)
240         self.tray.setToolTip('Electrum')
241         self.tray.activated.connect(self.tray_activated)
242
243         self.build_menu()
244         self.tray.show()
245         self.create_status_bar()
246
247         self.need_update = threading.Event()
248
249         self.expert_mode   = config.get('classic_expert_mode', False)
250         self.decimal_point = config.get('decimal_point', 8)
251         self.num_zeros     = int(config.get('num_zeros',0))
252         self.fee                   = int(config.get('fee_per_kb',20000))
253
254         set_language(config.get('language'))
255
256         self.funds_error = False
257         self.completions = QStringListModel()
258
259         self.tabs = tabs = QTabWidget(self)
260         self.column_widths = self.config.get("column_widths", default_column_widths )
261         tabs.addTab(self.create_history_tab(), _('History') )
262         tabs.addTab(self.create_send_tab(), _('Send') )
263         tabs.addTab(self.create_receive_tab(), _('Receive') )
264         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
265         tabs.addTab(self.create_console_tab(), _('Console') )
266         tabs.setMinimumSize(600, 400)
267         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
268         self.setCentralWidget(tabs)
269
270         g = self.config.get("winpos-qt",[100, 100, 840, 400])
271         self.setGeometry(g[0], g[1], g[2], g[3])
272
273         self.init_menubar()
274
275         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
276         QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
277         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
278         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
279         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
280         
281         self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
282         self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.wallet.interface.banner) )
283         self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
284         self.history_list.setFocus(True)
285         
286         self.exchanger = exchange_rate.Exchanger(self)
287         self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
288
289         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
290         if platform.system() == 'Windows':
291             n = 3 if self.wallet.seed else 2
292             tabs.setCurrentIndex (n)
293             tabs.setCurrentIndex (0)
294
295         # plugins that need to change the GUI do it here
296         self.run_hook('init')
297
298
299
300
301     def load_wallet(self, wallet):
302         import electrum
303         self.wallet = wallet
304
305         self.wallet.interface.register_callback('updated', lambda: self.need_update.set())
306         self.wallet.interface.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
307         self.wallet.interface.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
308         self.wallet.interface.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
309         self.wallet.interface.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
310         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.wallet.storage.path
311         if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
312         self.setWindowTitle( title )
313         self.update_wallet()
314         # set initial message
315         self.console.showMessage(self.wallet.interface.banner)
316         # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
317         self.notify_transactions()
318
319         # account selector
320         accounts = self.wallet.get_accounts()
321         self.account_selector.clear()
322         if len(accounts) > 1:
323             self.account_selector.addItems([_("All accounts")] + accounts.values())
324             self.account_selector.setCurrentIndex(0)
325             self.account_selector.show()
326         else:
327             self.account_selector.hide()
328
329         self.update_lock_icon()
330         self.update_buttons_on_seed()
331         self.update_console()
332
333
334     def select_wallet_file(self):
335         wallet_folder = self.wallet.storage.path
336         re.sub("(\/\w*.dat)$", "", wallet_folder)
337         file_name = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder, "*.dat") )
338         return file_name
339
340
341     def open_wallet(self):
342
343         filename = self.select_wallet_file()
344         if not filename:
345             return
346
347         storage = WalletStorage({'wallet_path': filename})
348         if not storage.file_exists:
349             self.show_message("file not found "+ filename)
350             return
351
352         interface = self.wallet.interface
353         blockchain = self.wallet.verifier.blockchain
354         self.wallet.stop_threads()
355         
356         # create new wallet 
357         wallet = Wallet(storage)
358         wallet.start_threads(interface, blockchain)
359
360         self.load_wallet(wallet)
361
362
363     def new_wallet(self):
364         import installwizard
365
366         wallet_folder = self.wallet.storage.path
367         re.sub("(\/\w*.dat)$", "", wallet_folder)
368         filename = self.getSaveFileName("Select your wallet file", wallet_folder, "*.dat")
369
370         storage = WalletStorage({'wallet_path': filename})
371         assert not storage.file_exists
372
373         wizard = installwizard.InstallWizard(self.config, self.wallet.interface, storage)
374         wallet = wizard.run()
375         if wallet: 
376             self.load_wallet(wallet)
377         
378
379
380     def init_menubar(self):
381         menubar = QMenuBar()
382
383         file_menu = menubar.addMenu(_("&File"))
384         open_wallet_action = file_menu.addAction(_("&Open"))
385         open_wallet_action.triggered.connect(self.open_wallet)
386
387         new_wallet_action = file_menu.addAction(_("&Create/Restore"))
388         new_wallet_action.triggered.connect(self.new_wallet)
389
390         wallet_backup = file_menu.addAction(_("&Copy"))
391         wallet_backup.triggered.connect(lambda: backup_wallet(self.config.path))
392
393         quit_item = file_menu.addAction(_("&Close"))
394         quit_item.triggered.connect(self.close)
395
396         wallet_menu = menubar.addMenu(_("&Wallet"))
397
398         # Settings / Preferences are all reserved keywords in OSX using this as work around
399         preferences_name = _("Electrum preferences") if sys.platform == 'darwin' else _("Preferences")
400         preferences_menu = wallet_menu.addAction(preferences_name)
401         preferences_menu.triggered.connect(self.settings_dialog)
402
403         wallet_menu.addSeparator()
404
405         raw_transaction_menu = wallet_menu.addMenu(_("&Load raw transaction"))
406
407         raw_transaction_file = raw_transaction_menu.addAction(_("&From file"))
408         raw_transaction_file.triggered.connect(self.do_process_from_file)
409
410         raw_transaction_text = raw_transaction_menu.addAction(_("&From text"))
411         raw_transaction_text.triggered.connect(self.do_process_from_text)
412
413         wallet_menu.addSeparator()
414
415         show_menu = wallet_menu.addMenu(_("Show"))
416
417         #if self.wallet.seed:
418         show_seed = show_menu.addAction(_("&Seed"))
419         show_seed.triggered.connect(self.show_seed_dialog)
420
421         show_mpk = show_menu.addAction(_("&Master Public Key"))
422         show_mpk.triggered.connect(self.show_master_public_key)
423
424         wallet_menu.addSeparator()
425         new_contact = wallet_menu.addAction(_("&New contact"))
426         new_contact.triggered.connect(self.new_contact_dialog)
427
428         new_account = wallet_menu.addAction(_("&New account"))
429         new_account.triggered.connect(self.new_account_dialog)
430
431         import_menu = menubar.addMenu(_("&Import"))
432         in_labels = import_menu.addAction(_("&Labels"))
433         in_labels.triggered.connect(self.do_import_labels)
434
435         in_private_keys = import_menu.addAction(_("&Private keys"))
436         in_private_keys.triggered.connect(self.do_import_privkey)
437
438         export_menu = menubar.addMenu(_("&Export"))
439         ex_private_keys = export_menu.addAction(_("&Private keys"))
440         ex_private_keys.triggered.connect(self.do_export_privkeys)
441
442         ex_history = export_menu.addAction(_("&History"))
443         ex_history.triggered.connect(self.do_export_history)
444
445         ex_labels = export_menu.addAction(_("&Labels"))
446         ex_labels.triggered.connect(self.do_export_labels)
447
448         help_menu = menubar.addMenu(_("&Help"))
449         doc_open = help_menu.addAction(_("&Documentation"))
450         doc_open.triggered.connect(lambda: webbrowser.open("http://electrum.org/documentation.html"))
451         web_open = help_menu.addAction(_("&Official website")) 
452         web_open.triggered.connect(lambda: webbrowser.open("http://electrum.org"))
453
454         self.setMenuBar(menubar)
455
456
457
458     def notify_transactions(self):
459         print_error("Notifying GUI")
460         if len(self.wallet.interface.pending_transactions_for_notifications) > 0:
461             # Combine the transactions if there are more then three
462             tx_amount = len(self.wallet.interface.pending_transactions_for_notifications)
463             if(tx_amount >= 3):
464                 total_amount = 0
465                 for tx in self.wallet.interface.pending_transactions_for_notifications:
466                     is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
467                     if(v > 0):
468                         total_amount += v
469
470                 self.notify("%s new transactions received. Total amount received in the new transactions %s %s" \
471                                 % (tx_amount, self.format_amount(total_amount), self.base_unit()))
472
473                 self.wallet.interface.pending_transactions_for_notifications = []
474             else:
475               for tx in self.wallet.interface.pending_transactions_for_notifications:
476                   if tx:
477                       self.wallet.interface.pending_transactions_for_notifications.remove(tx)
478                       is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
479                       if(v > 0):
480                           self.notify("New transaction received. %s %s" % (self.format_amount(v), self.base_unit()))
481
482     def notify(self, message):
483         self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
484
485     # plugins
486     def init_plugins(self):
487         import imp, pkgutil, __builtin__
488         if __builtin__.use_local_modules:
489             fp, pathname, description = imp.find_module('plugins')
490             plugin_names = [name for a, name, b in pkgutil.iter_modules([pathname])]
491             plugin_names = filter( lambda name: os.path.exists(os.path.join(pathname,name+'.py')), plugin_names)
492             imp.load_module('electrum_plugins', fp, pathname, description)
493             plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names)
494         else:
495             import electrum_plugins
496             plugin_names = [name for a, name, b in pkgutil.iter_modules(electrum_plugins.__path__)]
497             plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names]
498
499         self.plugins = []
500         for name, p in zip(plugin_names, plugins):
501             try:
502                 self.plugins.append( p.Plugin(self, name) )
503             except:
504                 print_msg("Error:cannot initialize plugin",p)
505                 traceback.print_exc(file=sys.stdout)
506
507
508     def run_hook(self, name, *args):
509         for p in self.plugins:
510             if not p.is_enabled():
511                 continue
512             try:
513                 f = eval('p.'+name)
514             except:
515                 continue
516             try:
517                 apply(f, args)
518             except:
519                 print_error("Plugin error")
520                 traceback.print_exc(file=sys.stdout)
521                 
522         return
523
524
525     def set_fee(self, fee):
526         if self.fee != fee:
527             self.fee = fee
528             self.config.set_key('fee_per_kb', self.fee, True)
529         
530
531     def set_label(self, name, text = None):
532         changed = False
533         old_text = self.wallet.labels.get(name)
534         if text:
535             if old_text != text:
536                 self.wallet.labels[name] = text
537                 self.wallet.storage.set_key('labels', self.wallet.labels)
538                 changed = True
539         else:
540             if old_text:
541                 self.wallet.labels.pop(name)
542                 changed = True
543         self.run_hook('set_label', name, text, changed)
544         return changed
545
546
547     # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
548     def getOpenFileName(self, title, filter = None):
549         directory = self.config.get('io_dir', os.path.expanduser('~'))
550         fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
551         if fileName and directory != os.path.dirname(fileName):
552             self.config.set_key('io_dir', os.path.dirname(fileName), True)
553         return fileName
554
555     def getSaveFileName(self, title, filename, filter = None):
556         directory = self.config.get('io_dir', os.path.expanduser('~'))
557         path = os.path.join( directory, filename )
558         fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
559         if fileName and directory != os.path.dirname(fileName):
560             self.config.set_key('io_dir', os.path.dirname(fileName), True)
561         return fileName
562
563
564
565     def close(self):
566         QMainWindow.close(self)
567         self.run_hook('close_main_window')
568
569     def connect_slots(self, sender):
570         self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
571         self.previous_payto_e=''
572
573     def timer_actions(self):
574         if self.need_update.is_set():
575             self.update_wallet()
576             self.need_update.clear()
577         self.run_hook('timer_actions')
578     
579     def format_amount(self, x, is_diff=False, whitespaces=False):
580         return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
581
582     def read_amount(self, x):
583         if x in['.', '']: return None
584         p = pow(10, self.decimal_point)
585         return int( p * Decimal(x) )
586
587     def base_unit(self):
588         assert self.decimal_point in [5,8]
589         return "BTC" if self.decimal_point == 8 else "mBTC"
590
591     def update_status(self):
592         if self.wallet.interface and self.wallet.interface.is_connected:
593             if not self.wallet.up_to_date:
594                 text = _("Synchronizing...")
595                 icon = QIcon(":icons/status_waiting.png")
596             else:
597                 c, u = self.wallet.get_account_balance(self.current_account)
598                 text =  _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
599                 if u: text +=  " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
600                 text += self.create_quote_text(Decimal(c+u)/100000000)
601                 self.tray.setToolTip(text)
602                 icon = QIcon(":icons/status_connected.png")
603         else:
604             text = _("Not connected")
605             icon = QIcon(":icons/status_disconnected.png")
606
607         self.balance_label.setText(text)
608         self.status_button.setIcon( icon )
609
610     def update_wallet(self):
611         self.update_status()
612         if self.wallet.up_to_date or not self.wallet.interface.is_connected:
613             self.update_history_tab()
614             self.update_receive_tab()
615             self.update_contacts_tab()
616             self.update_completions()
617
618
619     def create_quote_text(self, btc_balance):
620         quote_currency = self.config.get("currency", "None")
621         quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
622         if quote_balance is None:
623             quote_text = ""
624         else:
625             quote_text = "  (%.2f %s)" % (quote_balance, quote_currency)
626         return quote_text
627         
628     def create_history_tab(self):
629         self.history_list = l = MyTreeWidget(self)
630         l.setColumnCount(5)
631         for i,width in enumerate(self.column_widths['history']):
632             l.setColumnWidth(i, width)
633         l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
634         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
635         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
636
637         l.setContextMenuPolicy(Qt.CustomContextMenu)
638         l.customContextMenuRequested.connect(self.create_history_menu)
639         return l
640
641
642     def create_history_menu(self, position):
643         self.history_list.selectedIndexes() 
644         item = self.history_list.currentItem()
645         if not item: return
646         tx_hash = str(item.data(0, Qt.UserRole).toString())
647         if not tx_hash: return
648         menu = QMenu()
649         #menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
650         menu.addAction(_("Details"), lambda: self.show_tx_details(self.wallet.transactions.get(tx_hash)))
651         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
652         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
653
654
655     def show_tx_details(self, tx):
656         dialog = QDialog(self)
657         dialog.setModal(1)
658         dialog.setWindowTitle(_("Transaction Details"))
659         vbox = QVBoxLayout()
660         dialog.setLayout(vbox)
661         dialog.setMinimumSize(600,300)
662
663         tx_hash = tx.hash()
664         if tx_hash in self.wallet.transactions.keys():
665             is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
666             conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
667             if timestamp:
668                 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
669             else:
670                 time_str = 'pending'
671         else:
672             is_mine = False
673
674         vbox.addWidget(QLabel("Transaction ID:"))
675         e  = QLineEdit(tx_hash)
676         e.setReadOnly(True)
677         vbox.addWidget(e)
678
679         vbox.addWidget(QLabel("Date: %s"%time_str))
680         vbox.addWidget(QLabel("Status: %d confirmations"%conf))
681         if is_mine:
682             if fee is not None: 
683                 vbox.addWidget(QLabel("Amount sent: %s"% self.format_amount(v-fee)))
684                 vbox.addWidget(QLabel("Transaction fee: %s"% self.format_amount(fee)))
685             else:
686                 vbox.addWidget(QLabel("Amount sent: %s"% self.format_amount(v)))
687                 vbox.addWidget(QLabel("Transaction fee: unknown"))
688         else:
689             vbox.addWidget(QLabel("Amount received: %s"% self.format_amount(v)))
690
691         vbox.addWidget( self.generate_transaction_information_widget(tx) )
692
693         ok_button = QPushButton(_("Close"))
694         ok_button.setDefault(True)
695         ok_button.clicked.connect(dialog.accept)
696         
697         hbox = QHBoxLayout()
698         hbox.addStretch(1)
699         hbox.addWidget(ok_button)
700         vbox.addLayout(hbox)
701         dialog.exec_()
702
703     def tx_label_clicked(self, item, column):
704         if column==2 and item.isSelected():
705             self.is_edit=True
706             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
707             self.history_list.editItem( item, column )
708             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
709             self.is_edit=False
710
711     def tx_label_changed(self, item, column):
712         if self.is_edit: 
713             return
714         self.is_edit=True
715         tx_hash = str(item.data(0, Qt.UserRole).toString())
716         tx = self.wallet.transactions.get(tx_hash)
717         text = unicode( item.text(2) )
718         self.set_label(tx_hash, text) 
719         if text: 
720             item.setForeground(2, QBrush(QColor('black')))
721         else:
722             text = self.wallet.get_default_label(tx_hash)
723             item.setText(2, text)
724             item.setForeground(2, QBrush(QColor('gray')))
725         self.is_edit=False
726
727
728     def edit_label(self, is_recv):
729         l = self.receive_list if is_recv else self.contacts_list
730         item = l.currentItem()
731         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
732         l.editItem( item, 1 )
733         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
734
735
736
737     def address_label_clicked(self, item, column, l, column_addr, column_label):
738         if column == column_label and item.isSelected():
739             is_editable = item.data(0, 32).toBool()
740             if not is_editable:
741                 return
742             addr = unicode( item.text(column_addr) )
743             label = unicode( item.text(column_label) )
744             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
745             l.editItem( item, column )
746             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
747
748
749     def address_label_changed(self, item, column, l, column_addr, column_label):
750         if column == column_label:
751             addr = unicode( item.text(column_addr) )
752             text = unicode( item.text(column_label) )
753             is_editable = item.data(0, 32).toBool()
754             if not is_editable:
755                 return
756
757             changed = self.set_label(addr, text)
758             if changed:
759                 self.update_history_tab()
760                 self.update_completions()
761                 
762             self.current_item_changed(item)
763
764         self.run_hook('item_changed', item, column)
765
766
767     def current_item_changed(self, a):
768         self.run_hook('current_item_changed', a)
769
770
771
772     def update_history_tab(self):
773
774         self.history_list.clear()
775         for item in self.wallet.get_tx_history(self.current_account):
776             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
777             if conf > 0:
778                 try:
779                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
780                 except:
781                     time_str = "unknown"
782
783             if conf == -1:
784                 time_str = 'unverified'
785                 icon = QIcon(":icons/unconfirmed.png")
786             elif conf == 0:
787                 time_str = 'pending'
788                 icon = QIcon(":icons/unconfirmed.png")
789             elif conf < 6:
790                 icon = QIcon(":icons/clock%d.png"%conf)
791             else:
792                 icon = QIcon(":icons/confirmed.png")
793
794             if value is not None:
795                 v_str = self.format_amount(value, True, whitespaces=True)
796             else:
797                 v_str = '--'
798
799             balance_str = self.format_amount(balance, whitespaces=True)
800             
801             if tx_hash:
802                 label, is_default_label = self.wallet.get_label(tx_hash)
803             else:
804                 label = _('Pruned transaction outputs')
805                 is_default_label = False
806
807             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
808             item.setFont(2, QFont(MONOSPACE_FONT))
809             item.setFont(3, QFont(MONOSPACE_FONT))
810             item.setFont(4, QFont(MONOSPACE_FONT))
811             if value < 0:
812                 item.setForeground(3, QBrush(QColor("#BC1E1E")))
813             if tx_hash:
814                 item.setData(0, Qt.UserRole, tx_hash)
815                 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
816             if is_default_label:
817                 item.setForeground(2, QBrush(QColor('grey')))
818
819             item.setIcon(0, icon)
820             self.history_list.insertTopLevelItem(0,item)
821             
822
823         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
824
825
826     def create_send_tab(self):
827         w = QWidget()
828
829         grid = QGridLayout()
830         grid.setSpacing(8)
831         grid.setColumnMinimumWidth(3,300)
832         grid.setColumnStretch(5,1)
833
834
835         self.payto_e = QLineEdit()
836         grid.addWidget(QLabel(_('Pay to')), 1, 0)
837         grid.addWidget(self.payto_e, 1, 1, 1, 3)
838             
839         grid.addWidget(HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)')), 1, 4)
840
841         completer = QCompleter()
842         completer.setCaseSensitivity(False)
843         self.payto_e.setCompleter(completer)
844         completer.setModel(self.completions)
845
846         self.message_e = QLineEdit()
847         grid.addWidget(QLabel(_('Description')), 2, 0)
848         grid.addWidget(self.message_e, 2, 1, 1, 3)
849         grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')), 2, 4)
850
851         self.amount_e = AmountEdit(self.base_unit)
852         grid.addWidget(QLabel(_('Amount')), 3, 0)
853         grid.addWidget(self.amount_e, 3, 1, 1, 2)
854         grid.addWidget(HelpButton(
855                 _('Amount to be sent.') + '\n\n' \
856                     + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') \
857                     + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
858         
859         self.fee_e = AmountEdit(self.base_unit)
860         grid.addWidget(QLabel(_('Fee')), 4, 0)
861         grid.addWidget(self.fee_e, 4, 1, 1, 2) 
862         grid.addWidget(HelpButton(
863                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
864                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
865                     + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
866
867
868         self.send_button = EnterButton(_("Send"), self.do_send)
869         grid.addWidget(self.send_button, 6, 1)
870
871         b = EnterButton(_("Clear"),self.do_clear)
872         grid.addWidget(b, 6, 2)
873
874         self.payto_sig = QLabel('')
875         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
876
877         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
878         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
879         w.setLayout(grid) 
880
881         w2 = QWidget()
882         vbox = QVBoxLayout()
883         vbox.addWidget(w)
884         vbox.addStretch(1)
885         w2.setLayout(vbox)
886
887         def entry_changed( is_fee ):
888             self.funds_error = False
889
890             if self.amount_e.is_shortcut:
891                 self.amount_e.is_shortcut = False
892                 c, u = self.wallet.get_account_balance(self.current_account)
893                 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0, self.current_account)
894                 fee = self.wallet.estimated_fee(inputs)
895                 amount = c + u - fee
896                 self.amount_e.setText( self.format_amount(amount) )
897                 self.fee_e.setText( self.format_amount( fee ) )
898                 return
899                 
900             amount = self.read_amount(str(self.amount_e.text()))
901             fee = self.read_amount(str(self.fee_e.text()))
902
903             if not is_fee: fee = None
904             if amount is None:
905                 return
906             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, self.current_account )
907             if not is_fee:
908                 self.fee_e.setText( self.format_amount( fee ) )
909             if inputs:
910                 palette = QPalette()
911                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
912                 text = ""
913             else:
914                 palette = QPalette()
915                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
916                 self.funds_error = True
917                 text = _( "Not enough funds" )
918                 c, u = self.wallet.get_frozen_balance()
919                 if c+u: text += ' (' + self.format_amount(c+u).strip() + self.base_unit() + ' ' +_("are frozen") + ')'
920
921             self.statusBar().showMessage(text)
922             self.amount_e.setPalette(palette)
923             self.fee_e.setPalette(palette)
924
925         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
926         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
927
928         self.run_hook('create_send_tab', grid)
929         return w2
930
931
932     def update_completions(self):
933         l = []
934         for addr,label in self.wallet.labels.items():
935             if addr in self.wallet.addressbook:
936                 l.append( label + '  <' + addr + '>')
937
938         self.run_hook('update_completions', l)
939         self.completions.setStringList(l)
940
941
942     def protected(func):
943         return lambda s, *args: s.do_protect(func, args)
944
945
946     def do_send(self):
947
948         label = unicode( self.message_e.text() )
949         r = unicode( self.payto_e.text() )
950         r = r.strip()
951
952         # label or alias, with address in brackets
953         m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
954         to_address = m.group(2) if m else r
955
956         if not is_valid(to_address):
957             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
958             return
959
960         try:
961             amount = self.read_amount(unicode( self.amount_e.text()))
962         except:
963             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
964             return
965         try:
966             fee = self.read_amount(unicode( self.fee_e.text()))
967         except:
968             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
969             return
970
971         confirm_amount = self.config.get('confirm_amount', 100000000)
972         if amount >= confirm_amount:
973             if not self.question("send %s to %s?"%(self.format_amount(amount) + ' '+ self.base_unit(), to_address)):
974                 return
975
976         self.send_tx(to_address, amount, fee, label)
977
978
979     @protected
980     def send_tx(self, to_address, amount, fee, label, password):
981
982         try:
983             tx = self.wallet.mktx( [(to_address, amount)], password, fee, account=self.current_account)
984         except BaseException, e:
985             traceback.print_exc(file=sys.stdout)
986             self.show_message(str(e))
987             return
988
989         if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
990             QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
991             return
992
993         self.run_hook('send_tx', tx)
994
995         if label: 
996             self.set_label(tx.hash(), label)
997
998         if tx.is_complete:
999             h = self.wallet.send_tx(tx)
1000             waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
1001             status, msg = self.wallet.receive_tx( h )
1002             if status:
1003                 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
1004                 self.do_clear()
1005                 self.update_contacts_tab()
1006             else:
1007                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
1008         else:
1009             filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
1010             try:
1011                 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
1012                 with open(fileName,'w') as f:
1013                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1014                 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
1015             except:
1016                 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
1017
1018
1019
1020
1021     def set_url(self, url):
1022         address, amount, label, message, signature, identity, url = util.parse_url(url)
1023         if self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
1024
1025         if label and self.wallet.labels.get(address) != label:
1026             if self.question('Give label "%s" to address %s ?'%(label,address)):
1027                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
1028                     self.wallet.addressbook.append(address)
1029                 self.set_label(address, label)
1030
1031         self.run_hook('set_url', url, self.show_message, self.question)
1032
1033         self.tabs.setCurrentIndex(1)
1034         label = self.wallet.labels.get(address)
1035         m_addr = label + '  <'+ address +'>' if label else address
1036         self.payto_e.setText(m_addr)
1037
1038         self.message_e.setText(message)
1039         self.amount_e.setText(amount)
1040         if identity:
1041             self.set_frozen(self.payto_e,True)
1042             self.set_frozen(self.amount_e,True)
1043             self.set_frozen(self.message_e,True)
1044             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
1045         else:
1046             self.payto_sig.setVisible(False)
1047
1048     def do_clear(self):
1049         self.payto_sig.setVisible(False)
1050         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
1051             e.setText('')
1052             self.set_frozen(e,False)
1053         self.update_status()
1054
1055     def set_frozen(self,entry,frozen):
1056         if frozen:
1057             entry.setReadOnly(True)
1058             entry.setFrame(False)
1059             palette = QPalette()
1060             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
1061             entry.setPalette(palette)
1062         else:
1063             entry.setReadOnly(False)
1064             entry.setFrame(True)
1065             palette = QPalette()
1066             palette.setColor(entry.backgroundRole(), QColor('white'))
1067             entry.setPalette(palette)
1068
1069
1070     def toggle_freeze(self,addr):
1071         if not addr: return
1072         if addr in self.wallet.frozen_addresses:
1073             self.wallet.unfreeze(addr)
1074         else:
1075             self.wallet.freeze(addr)
1076         self.update_receive_tab()
1077
1078     def toggle_priority(self,addr):
1079         if not addr: return
1080         if addr in self.wallet.prioritized_addresses:
1081             self.wallet.unprioritize(addr)
1082         else:
1083             self.wallet.prioritize(addr)
1084         self.update_receive_tab()
1085
1086
1087     def create_list_tab(self, headers):
1088         "generic tab creation method"
1089         l = MyTreeWidget(self)
1090         l.setColumnCount( len(headers) )
1091         l.setHeaderLabels( headers )
1092
1093         w = QWidget()
1094         vbox = QVBoxLayout()
1095         w.setLayout(vbox)
1096
1097         vbox.setMargin(0)
1098         vbox.setSpacing(0)
1099         vbox.addWidget(l)
1100         buttons = QWidget()
1101         vbox.addWidget(buttons)
1102
1103         hbox = QHBoxLayout()
1104         hbox.setMargin(0)
1105         hbox.setSpacing(0)
1106         buttons.setLayout(hbox)
1107
1108         return l,w,hbox
1109
1110
1111     def create_receive_tab(self):
1112         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1113         l.setContextMenuPolicy(Qt.CustomContextMenu)
1114         l.customContextMenuRequested.connect(self.create_receive_menu)
1115         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1116         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1117         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1118         self.receive_list = l
1119         self.receive_buttons_hbox = hbox
1120         hbox.addStretch(1)
1121         return w
1122
1123
1124     def receive_tab_set_mode(self, i):
1125         self.save_column_widths()
1126         self.expert_mode = (i == 1)
1127         self.config.set_key('classic_expert_mode', self.expert_mode, True)
1128         self.update_receive_tab()
1129
1130
1131     def save_column_widths(self):
1132         if not self.expert_mode:
1133             widths = [ self.receive_list.columnWidth(0) ]
1134         else:
1135             widths = []
1136             for i in range(self.receive_list.columnCount() -1):
1137                 widths.append(self.receive_list.columnWidth(i))
1138         self.column_widths["receive"][self.expert_mode] = widths
1139         
1140         self.column_widths["history"] = []
1141         for i in range(self.history_list.columnCount() - 1):
1142             self.column_widths["history"].append(self.history_list.columnWidth(i))
1143
1144         self.column_widths["contacts"] = []
1145         for i in range(self.contacts_list.columnCount() - 1):
1146             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1147
1148         self.config.set_key("column_widths", self.column_widths, True)
1149
1150
1151     def create_contacts_tab(self):
1152         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1153         l.setContextMenuPolicy(Qt.CustomContextMenu)
1154         l.customContextMenuRequested.connect(self.create_contact_menu)
1155         for i,width in enumerate(self.column_widths['contacts']):
1156             l.setColumnWidth(i, width)
1157
1158         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1159         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1160         self.contacts_list = l
1161         self.contacts_buttons_hbox = hbox
1162         hbox.addStretch(1)
1163         return w
1164
1165
1166     def delete_imported_key(self, addr):
1167         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1168             self.wallet.delete_imported_key(addr)
1169             self.update_receive_tab()
1170             self.update_history_tab()
1171
1172
1173     def create_receive_menu(self, position):
1174         # fixme: this function apparently has a side effect.
1175         # if it is not called the menu pops up several times
1176         #self.receive_list.selectedIndexes() 
1177
1178         item = self.receive_list.itemAt(position)
1179         if not item: return
1180         addr = unicode(item.text(0))
1181         if not is_valid(addr): 
1182             item.setExpanded(not item.isExpanded())
1183             return 
1184         menu = QMenu()
1185         menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1186         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1187         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1188         menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1189         menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1190         if addr in self.wallet.imported_keys:
1191             menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1192
1193         if self.expert_mode:
1194             t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1195             menu.addAction(t, lambda: self.toggle_freeze(addr))
1196             t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1197             menu.addAction(t, lambda: self.toggle_priority(addr))
1198             
1199         self.run_hook('receive_menu', menu)
1200         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1201
1202
1203     def payto(self, addr):
1204         if not addr: return
1205         label = self.wallet.labels.get(addr)
1206         m_addr = label + '  <' + addr + '>' if label else addr
1207         self.tabs.setCurrentIndex(1)
1208         self.payto_e.setText(m_addr)
1209         self.amount_e.setFocus()
1210
1211
1212     def delete_contact(self, x):
1213         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1214             self.wallet.delete_contact(x)
1215             self.set_label(x, None)
1216             self.update_history_tab()
1217             self.update_contacts_tab()
1218             self.update_completions()
1219
1220
1221     def create_contact_menu(self, position):
1222         item = self.contacts_list.itemAt(position)
1223         if not item: return
1224         addr = unicode(item.text(0))
1225         label = unicode(item.text(1))
1226         is_editable = item.data(0,32).toBool()
1227         payto_addr = item.data(0,33).toString()
1228         menu = QMenu()
1229         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1230         menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1231         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1232         if is_editable:
1233             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1234             menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1235
1236         self.run_hook('create_contact_menu', menu, item)
1237         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1238
1239
1240     def update_receive_item(self, item):
1241         item.setFont(0, QFont(MONOSPACE_FONT))
1242         address = str(item.data(0,0).toString())
1243         label = self.wallet.labels.get(address,'')
1244         item.setData(1,0,label)
1245         item.setData(0,32, True) # is editable
1246
1247         self.run_hook('update_receive_item', address, item)
1248                 
1249         c, u = self.wallet.get_addr_balance(address)
1250         balance = self.format_amount(c + u)
1251         item.setData(2,0,balance)
1252
1253         if self.expert_mode:
1254             if address in self.wallet.frozen_addresses: 
1255                 item.setBackgroundColor(0, QColor('lightblue'))
1256             elif address in self.wallet.prioritized_addresses: 
1257                 item.setBackgroundColor(0, QColor('lightgreen'))
1258         
1259
1260     def update_receive_tab(self):
1261         l = self.receive_list
1262         
1263         l.clear()
1264         l.setColumnHidden(2, not self.expert_mode)
1265         l.setColumnHidden(3, not self.expert_mode)
1266         for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1267             l.setColumnWidth(i, width)
1268
1269         if self.current_account is None:
1270             account_items = self.wallet.accounts.items()
1271         elif self.current_account != -1:
1272             account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1273         else:
1274             account_items = []
1275
1276         for k, account in account_items:
1277             name = self.wallet.labels.get(k, 'unnamed account')
1278             c,u = self.wallet.get_account_balance(k)
1279             account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1280             l.addTopLevelItem(account_item)
1281             account_item.setExpanded(True)
1282             
1283             for is_change in ([0,1] if self.expert_mode else [0]):
1284                 if self.expert_mode:
1285                     name = "Receiving" if not is_change else "Change"
1286                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1287                     account_item.addChild(seq_item)
1288                     if not is_change: seq_item.setExpanded(True)
1289                 else:
1290                     seq_item = account_item
1291                 is_red = False
1292                 gap = 0
1293
1294                 for address in account.get_addresses(is_change):
1295                     h = self.wallet.history.get(address,[])
1296             
1297                     if h == []:
1298                         gap += 1
1299                         if gap > self.wallet.gap_limit:
1300                             is_red = True
1301                     else:
1302                         gap = 0
1303
1304                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1305                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1306                     self.update_receive_item(item)
1307                     if is_red:
1308                         item.setBackgroundColor(1, QColor('red'))
1309                     seq_item.addChild(item)
1310
1311
1312         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1313             c,u = self.wallet.get_imported_balance()
1314             account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1315             l.addTopLevelItem(account_item)
1316             account_item.setExpanded(True)
1317             for address in self.wallet.imported_keys.keys():
1318                 item = QTreeWidgetItem( [ address, '', '', ''] )
1319                 self.update_receive_item(item)
1320                 account_item.addChild(item)
1321                 
1322
1323         # we use column 1 because column 0 may be hidden
1324         l.setCurrentItem(l.topLevelItem(0),1)
1325
1326
1327     def update_contacts_tab(self):
1328         l = self.contacts_list
1329         l.clear()
1330
1331         for address in self.wallet.addressbook:
1332             label = self.wallet.labels.get(address,'')
1333             n = self.wallet.get_num_tx(address)
1334             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1335             item.setFont(0, QFont(MONOSPACE_FONT))
1336             # 32 = label can be edited (bool)
1337             item.setData(0,32, True)
1338             # 33 = payto string
1339             item.setData(0,33, address)
1340             l.addTopLevelItem(item)
1341
1342         self.run_hook('update_contacts_tab', l)
1343         l.setCurrentItem(l.topLevelItem(0))
1344
1345
1346
1347     def create_console_tab(self):
1348         from qt_console import Console
1349         self.console = console = Console()
1350         return console
1351
1352
1353     def update_console(self):
1354         console = self.console
1355         console.history = self.config.get("console-history",[])
1356         console.history_index = len(console.history)
1357
1358         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1359         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1360
1361         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1362         methods = {}
1363         def mkfunc(f, method):
1364             return lambda *args: apply( f, (method, args, self.password_dialog ))
1365         for m in dir(c):
1366             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1367             methods[m] = mkfunc(c._run, m)
1368             
1369         console.updateNamespace(methods)
1370
1371
1372     def change_account(self,s):
1373         if s == _("All accounts"):
1374             self.current_account = None
1375         else:
1376             accounts = self.wallet.get_accounts()
1377             for k, v in accounts.items():
1378                 if v == s:
1379                     self.current_account = k
1380         self.update_history_tab()
1381         self.update_status()
1382         self.update_receive_tab()
1383
1384     def create_status_bar(self):
1385
1386         sb = QStatusBar()
1387         sb.setFixedHeight(35)
1388         qtVersion = qVersion()
1389
1390         self.balance_label = QLabel("")
1391         sb.addWidget(self.balance_label)
1392
1393         update_notification = UpdateLabel(self.config)
1394         if(update_notification.new_version):
1395             sb.addPermanentWidget(update_notification)
1396
1397         self.account_selector = QComboBox()
1398         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account) 
1399         sb.addPermanentWidget(self.account_selector)
1400
1401         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1402             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1403
1404         self.lock_icon = QIcon()
1405         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1406         sb.addPermanentWidget( self.password_button )
1407             
1408         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1409         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) 
1410         sb.addPermanentWidget( self.seed_button )
1411         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog ) 
1412         sb.addPermanentWidget( self.status_button )
1413
1414         self.run_hook('create_status_bar', (sb,))
1415
1416         self.setStatusBar(sb)
1417
1418
1419     def update_lock_icon(self):
1420         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1421         self.password_button.setIcon( icon )
1422
1423
1424     def update_buttons_on_seed(self):
1425         if self.wallet.seed:
1426            self.seed_button.show()
1427            self.password_button.show()
1428            self.send_button.setText(_("Send"))
1429         else:
1430            self.password_button.hide()
1431            self.seed_button.hide()
1432            self.send_button.setText(_("Create unsigned transaction"))
1433
1434
1435     def change_password_dialog(self):
1436         from password_dialog import PasswordDialog
1437         d = PasswordDialog(self.wallet, self)
1438         d.run()
1439         self.update_lock_icon()
1440
1441         
1442     def go_lite(self):
1443         import gui_lite
1444         self.config.set_key('gui', 'lite', True)
1445         self.hide()
1446         if self.lite:
1447             self.lite.mini.show()
1448         else:
1449             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1450             self.lite.main(None)
1451
1452
1453     def new_contact_dialog(self):
1454         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1455         address = unicode(text)
1456         if ok:
1457             if is_valid(address):
1458                 self.wallet.add_contact(address)
1459                 self.update_contacts_tab()
1460                 self.update_history_tab()
1461                 self.update_completions()
1462             else:
1463                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1464
1465
1466     def new_account_dialog(self):
1467
1468         dialog = QDialog(self)
1469         dialog.setModal(1)
1470         dialog.setWindowTitle(_("New Account"))
1471
1472         addr = self.wallet.new_account_address()
1473         vbox = QVBoxLayout()
1474         vbox.addWidget(QLabel(_("To create a new account, please send coins to the first address of that account:")))
1475         e = QLineEdit(addr)
1476         e.setReadOnly(True)
1477         vbox.addWidget(e)
1478
1479         ok_button = QPushButton(_("OK"))
1480         ok_button.setDefault(True)
1481         ok_button.clicked.connect(dialog.accept)
1482
1483         hbox = QHBoxLayout()
1484         hbox.addStretch(1)
1485         hbox.addWidget(ok_button)
1486         vbox.addLayout(hbox)
1487
1488         dialog.setLayout(vbox)
1489         dialog.exec_()
1490
1491             
1492
1493     def show_master_public_key(self):
1494         dialog = QDialog(self)
1495         dialog.setModal(1)
1496         dialog.setWindowTitle(_("Master Public Key"))
1497
1498         main_text = QTextEdit()
1499         main_text.setText(self.wallet.get_master_public_key())
1500         main_text.setReadOnly(True)
1501         main_text.setMaximumHeight(170)
1502         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1503
1504         ok_button = QPushButton(_("OK"))
1505         ok_button.setDefault(True)
1506         ok_button.clicked.connect(dialog.accept)
1507
1508         main_layout = QGridLayout()
1509         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1510
1511         main_layout.addWidget(main_text, 1, 0)
1512         main_layout.addWidget(qrw, 1, 1 )
1513
1514         vbox = QVBoxLayout()
1515         vbox.addLayout(main_layout)
1516         hbox = QHBoxLayout()
1517         hbox.addStretch(1)
1518         hbox.addWidget(ok_button)
1519         vbox.addLayout(hbox)
1520
1521         dialog.setLayout(vbox)
1522         dialog.exec_()
1523         
1524
1525     @protected
1526     def show_seed_dialog(self, password):
1527         if not self.wallet.seed:
1528             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1529             return
1530         try:
1531             seed = self.wallet.decode_seed(password)
1532         except:
1533             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1534             return
1535
1536         from seed_dialog import SeedDialog
1537         d = SeedDialog(self)
1538         d.show_seed(seed, self.wallet.imported_keys)
1539
1540
1541
1542     def show_qrcode(self, data, title = "QR code"):
1543         if not data: return
1544         d = QDialog(self)
1545         d.setModal(1)
1546         d.setWindowTitle(title)
1547         d.setMinimumSize(270, 300)
1548         vbox = QVBoxLayout()
1549         qrw = QRCodeWidget(data)
1550         vbox.addWidget(qrw, 1)
1551         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1552         hbox = QHBoxLayout()
1553         hbox.addStretch(1)
1554
1555         def print_qr(self):
1556             filename = "qrcode.bmp"
1557             bmp.save_qrcode(qrw.qr, filename)
1558             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1559
1560         b = QPushButton(_("Save"))
1561         hbox.addWidget(b)
1562         b.clicked.connect(print_qr)
1563
1564         b = QPushButton(_("Close"))
1565         hbox.addWidget(b)
1566         b.clicked.connect(d.accept)
1567         b.setDefault(True)
1568
1569         vbox.addLayout(hbox)
1570         d.setLayout(vbox)
1571         d.exec_()
1572
1573
1574     def do_protect(self, func, args):
1575         if self.wallet.use_encryption:
1576             password = self.password_dialog()
1577             if not password:
1578                 return
1579         else:
1580             password = None
1581             
1582         if args != (False,):
1583             args = (self,) + args + (password,)
1584         else:
1585             args = (self,password)
1586         apply( func, args)
1587
1588
1589     @protected
1590     def show_private_key(self, address, password):
1591         if not address: return
1592         try:
1593             pk = self.wallet.get_private_key(address, password)
1594         except BaseException, e:
1595             self.show_message(str(e))
1596             return
1597         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1598
1599
1600     @protected
1601     def do_sign(self, address, message, signature, password):
1602         try:
1603             sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1604             signature.setText(sig)
1605         except BaseException, e:
1606             self.show_message(str(e))
1607
1608     def sign_message(self, address):
1609         if not address: return
1610         d = QDialog(self)
1611         d.setModal(1)
1612         d.setWindowTitle(_('Sign Message'))
1613         d.setMinimumSize(410, 290)
1614
1615         tab_widget = QTabWidget()
1616         tab = QWidget()
1617         layout = QGridLayout(tab)
1618
1619         sign_address = QLineEdit()
1620
1621         sign_address.setText(address)
1622         layout.addWidget(QLabel(_('Address')), 1, 0)
1623         layout.addWidget(sign_address, 1, 1)
1624
1625         sign_message = QTextEdit()
1626         layout.addWidget(QLabel(_('Message')), 2, 0)
1627         layout.addWidget(sign_message, 2, 1)
1628         layout.setRowStretch(2,3)
1629
1630         sign_signature = QTextEdit()
1631         layout.addWidget(QLabel(_('Signature')), 3, 0)
1632         layout.addWidget(sign_signature, 3, 1)
1633         layout.setRowStretch(3,1)
1634
1635
1636         hbox = QHBoxLayout()
1637         b = QPushButton(_("Sign"))
1638         hbox.addWidget(b)
1639         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1640         b = QPushButton(_("Close"))
1641         b.clicked.connect(d.accept)
1642         hbox.addWidget(b)
1643         layout.addLayout(hbox, 4, 1)
1644         tab_widget.addTab(tab, _("Sign"))
1645
1646
1647         tab = QWidget()
1648         layout = QGridLayout(tab)
1649
1650         verify_address = QLineEdit()
1651         layout.addWidget(QLabel(_('Address')), 1, 0)
1652         layout.addWidget(verify_address, 1, 1)
1653
1654         verify_message = QTextEdit()
1655         layout.addWidget(QLabel(_('Message')), 2, 0)
1656         layout.addWidget(verify_message, 2, 1)
1657         layout.setRowStretch(2,3)
1658
1659         verify_signature = QTextEdit()
1660         layout.addWidget(QLabel(_('Signature')), 3, 0)
1661         layout.addWidget(verify_signature, 3, 1)
1662         layout.setRowStretch(3,1)
1663
1664         def do_verify():
1665             if self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText())):
1666                 self.show_message(_("Signature verified"))
1667             else:
1668                 self.show_message(_("Error: wrong signature"))
1669
1670         hbox = QHBoxLayout()
1671         b = QPushButton(_("Verify"))
1672         b.clicked.connect(do_verify)
1673         hbox.addWidget(b)
1674         b = QPushButton(_("Close"))
1675         b.clicked.connect(d.accept)
1676         hbox.addWidget(b)
1677         layout.addLayout(hbox, 4, 1)
1678         tab_widget.addTab(tab, _("Verify"))
1679
1680         vbox = QVBoxLayout()
1681         vbox.addWidget(tab_widget)
1682         d.setLayout(vbox)
1683         d.exec_()
1684
1685         
1686
1687
1688     def question(self, msg):
1689         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1690
1691     def show_message(self, msg):
1692         QMessageBox.information(self, _('Message'), msg, _('OK'))
1693
1694     def password_dialog(self ):
1695         d = QDialog(self)
1696         d.setModal(1)
1697
1698         pw = QLineEdit()
1699         pw.setEchoMode(2)
1700
1701         vbox = QVBoxLayout()
1702         msg = _('Please enter your password')
1703         vbox.addWidget(QLabel(msg))
1704
1705         grid = QGridLayout()
1706         grid.setSpacing(8)
1707         grid.addWidget(QLabel(_('Password')), 1, 0)
1708         grid.addWidget(pw, 1, 1)
1709         vbox.addLayout(grid)
1710
1711         vbox.addLayout(ok_cancel_buttons(d))
1712         d.setLayout(vbox)
1713
1714         self.run_hook('password_dialog', pw, grid, 1)
1715         if not d.exec_(): return
1716         return unicode(pw.text())
1717
1718
1719
1720
1721
1722
1723     def generate_transaction_information_widget(self, tx):
1724         tabs = QTabWidget(self)
1725
1726         tab1 = QWidget()
1727         grid_ui = QGridLayout(tab1)
1728         grid_ui.setColumnStretch(0,1)
1729         tabs.addTab(tab1, _('Outputs') )
1730
1731         tree_widget = MyTreeWidget(self)
1732         tree_widget.setColumnCount(2)
1733         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1734         tree_widget.setColumnWidth(0, 300)
1735         tree_widget.setColumnWidth(1, 50)
1736
1737         for address, value in tx.outputs:
1738             item = QTreeWidgetItem( [address, "%s" % ( self.format_amount(value))] )
1739             tree_widget.addTopLevelItem(item)
1740
1741         tree_widget.setMaximumHeight(100)
1742
1743         grid_ui.addWidget(tree_widget)
1744
1745         tab2 = QWidget()
1746         grid_ui = QGridLayout(tab2)
1747         grid_ui.setColumnStretch(0,1)
1748         tabs.addTab(tab2, _('Inputs') )
1749         
1750         tree_widget = MyTreeWidget(self)
1751         tree_widget.setColumnCount(2)
1752         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1753
1754         for input_line in tx.inputs:
1755             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1756             tree_widget.addTopLevelItem(item)
1757
1758         tree_widget.setMaximumHeight(100)
1759
1760         grid_ui.addWidget(tree_widget)
1761         return tabs
1762
1763
1764     def tx_dict_from_text(self, txt):
1765         try:
1766             tx_dict = json.loads(str(txt))
1767             assert "hex" in tx_dict.keys()
1768             assert "complete" in tx_dict.keys()
1769             if not tx_dict["complete"]:
1770                 assert "input_info" in tx_dict.keys()
1771         except:
1772             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1773             return None
1774         return tx_dict
1775
1776
1777     def read_tx_from_file(self):
1778         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1779         if not fileName:
1780             return
1781         try:
1782             with open(fileName, "r") as f:
1783                 file_content = f.read()
1784         except (ValueError, IOError, os.error), reason:
1785             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1786
1787         return self.tx_dict_from_text(file_content)
1788
1789
1790     @protected
1791     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
1792         try:
1793             self.wallet.signrawtransaction(tx, input_info, [], password)
1794             
1795             fileName = self.getSaveFileName(_("Select where to save your signed transaction"), 'signed_%s.txn' % (tx.hash()[0:8]), "*.txn")
1796             if fileName:
1797                 with open(fileName, "w+") as f:
1798                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1799                 self.show_message(_("Transaction saved successfully"))
1800                 if dialog:
1801                     dialog.done(0)
1802         except BaseException, e:
1803             self.show_message(str(e))
1804     
1805
1806     def send_raw_transaction(self, raw_tx, dialog = ""):
1807         result, result_message = self.wallet.sendtx( raw_tx )
1808         if result:
1809             self.show_message("Transaction successfully sent: %s" % (result_message))
1810             if dialog:
1811                 dialog.done(0)
1812         else:
1813             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1814
1815     def do_process_from_text(self):
1816         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1817         if not text:
1818             return
1819         tx_dict = self.tx_dict_from_text(text)
1820         if tx_dict:
1821             self.create_process_transaction_window(tx_dict)
1822
1823     def do_process_from_file(self):
1824         tx_dict = self.read_tx_from_file()
1825         if tx_dict: 
1826             self.create_process_transaction_window(tx_dict)
1827
1828     def create_process_transaction_window(self, tx_dict):
1829         tx = Transaction(tx_dict["hex"])
1830             
1831         dialog = QDialog(self)
1832         dialog.setMinimumWidth(500)
1833         dialog.setWindowTitle(_('Process raw transaction'))
1834         dialog.setModal(1)
1835
1836         l = QGridLayout()
1837         dialog.setLayout(l)
1838
1839         l.addWidget(QLabel(_("Transaction status:")), 3,0)
1840         l.addWidget(QLabel(_("Actions")), 4,0)
1841
1842         if tx_dict["complete"] == False:
1843             l.addWidget(QLabel(_("Unsigned")), 3,1)
1844             if self.wallet.seed :
1845                 b = QPushButton("Sign transaction")
1846                 input_info = json.loads(tx_dict["input_info"])
1847                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
1848                 l.addWidget(b, 4, 1)
1849             else:
1850                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
1851         else:
1852             l.addWidget(QLabel(_("Signed")), 3,1)
1853             b = QPushButton("Broadcast transaction")
1854             b.clicked.connect(lambda: self.send_raw_transaction(tx, dialog))
1855             l.addWidget(b,4,1)
1856
1857         l.addWidget( self.generate_transaction_information_widget(tx), 0,0,2,3)
1858         cancelButton = QPushButton(_("Cancel"))
1859         cancelButton.clicked.connect(lambda: dialog.done(0))
1860         l.addWidget(cancelButton, 4,2)
1861
1862         dialog.exec_()
1863
1864
1865     @protected
1866     def do_export_privkeys(self, password):
1867         self.show_message("%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),  _("Exposing a single private key can compromise your entire wallet!"), _("In particular, DO NOT use 'redeem private key' services proposed by third parties.")))
1868
1869         try:
1870             select_export = _('Select file to export your private keys to')
1871             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1872             if fileName:
1873                 with open(fileName, "w+") as csvfile:
1874                     transaction = csv.writer(csvfile)
1875                     transaction.writerow(["address", "private_key"])
1876
1877                     
1878                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1879                         transaction.writerow(["%34s"%addr,pk])
1880
1881                     self.show_message(_("Private keys exported."))
1882
1883         except (IOError, os.error), reason:
1884             export_error_label = _("Electrum was unable to produce a private key-export.")
1885             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1886
1887         except BaseException, e:
1888           self.show_message(str(e))
1889           return
1890
1891
1892     def do_import_labels(self):
1893         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1894         if not labelsFile: return
1895         try:
1896             f = open(labelsFile, 'r')
1897             data = f.read()
1898             f.close()
1899             for key, value in json.loads(data).items():
1900                 self.wallet.labels[key] = value
1901             self.wallet.save()
1902             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1903         except (IOError, os.error), reason:
1904             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1905             
1906
1907     def do_export_labels(self):
1908         labels = self.wallet.labels
1909         try:
1910             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1911             if fileName:
1912                 with open(fileName, 'w+') as f:
1913                     json.dump(labels, f)
1914                 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1915         except (IOError, os.error), reason:
1916             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1917
1918
1919     def do_export_history(self):
1920         from gui_lite import csv_transaction
1921         csv_transaction(self.wallet)
1922
1923
1924     @protected
1925     def do_import_privkey(self, password):
1926         if not self.wallet.imported_keys:
1927             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1928                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1929                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1930             if r == 4: return
1931
1932         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1933         if not text: return
1934
1935         text = str(text).split()
1936         badkeys = []
1937         addrlist = []
1938         for key in text:
1939             try:
1940                 addr = self.wallet.import_key(key, password)
1941             except BaseException as e:
1942                 badkeys.append(key)
1943                 continue
1944             if not addr: 
1945                 badkeys.append(key)
1946             else:
1947                 addrlist.append(addr)
1948         if addrlist:
1949             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1950         if badkeys:
1951             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1952         self.update_receive_tab()
1953         self.update_history_tab()
1954
1955
1956     def settings_dialog(self):
1957         d = QDialog(self)
1958         d.setWindowTitle(_('Electrum Settings'))
1959         d.setModal(1)
1960         vbox = QVBoxLayout()
1961
1962         tabs = QTabWidget(self)
1963         self.settings_tab = tabs
1964         vbox.addWidget(tabs)
1965
1966         tab1 = QWidget()
1967         grid_ui = QGridLayout(tab1)
1968         grid_ui.setColumnStretch(0,1)
1969         tabs.addTab(tab1, _('Display') )
1970
1971         nz_label = QLabel(_('Display zeros'))
1972         grid_ui.addWidget(nz_label, 0, 0)
1973         nz_e = AmountEdit(None,True)
1974         nz_e.setText("%d"% self.num_zeros)
1975         grid_ui.addWidget(nz_e, 0, 1)
1976         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1977         grid_ui.addWidget(HelpButton(msg), 0, 2)
1978         if not self.config.is_modifiable('num_zeros'):
1979             for w in [nz_e, nz_label]: w.setEnabled(False)
1980         
1981         lang_label=QLabel(_('Language') + ':')
1982         grid_ui.addWidget(lang_label, 1, 0)
1983         lang_combo = QComboBox()
1984         from i18n import languages
1985         lang_combo.addItems(languages.values())
1986         try:
1987             index = languages.keys().index(self.config.get("language",''))
1988         except:
1989             index = 0
1990         lang_combo.setCurrentIndex(index)
1991         grid_ui.addWidget(lang_combo, 1, 1)
1992         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1993         if not self.config.is_modifiable('language'):
1994             for w in [lang_combo, lang_label]: w.setEnabled(False)
1995
1996         currencies = self.exchanger.get_currencies()
1997         currencies.insert(0, "None")
1998
1999         cur_label=QLabel(_('Currency') + ':')
2000         grid_ui.addWidget(cur_label , 2, 0)
2001         cur_combo = QComboBox()
2002         cur_combo.addItems(currencies)
2003         try:
2004             index = currencies.index(self.config.get('currency', "None"))
2005         except:
2006             index = 0
2007         cur_combo.setCurrentIndex(index)
2008         grid_ui.addWidget(cur_combo, 2, 1)
2009         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
2010         
2011         expert_cb = QCheckBox(_('Expert mode'))
2012         expert_cb.setChecked(self.expert_mode)
2013         grid_ui.addWidget(expert_cb, 3, 0)
2014         hh =  _('In expert mode, your client will:') + '\n'  \
2015             + _(' - Show change addresses in the Receive tab') + '\n'  \
2016             + _(' - Display the balance of each address') + '\n'  \
2017             + _(' - Add freeze/prioritize actions to addresses.') 
2018         grid_ui.addWidget(HelpButton(hh), 3, 2)
2019         grid_ui.setRowStretch(4,1)
2020
2021         # wallet tab
2022         tab2 = QWidget()
2023         grid_wallet = QGridLayout(tab2)
2024         grid_wallet.setColumnStretch(0,1)
2025         tabs.addTab(tab2, _('Wallet') )
2026         
2027         fee_label = QLabel(_('Transaction fee'))
2028         grid_wallet.addWidget(fee_label, 0, 0)
2029         fee_e = AmountEdit(self.base_unit)
2030         fee_e.setText(self.format_amount(self.fee).strip())
2031         grid_wallet.addWidget(fee_e, 0, 2)
2032         msg = _('Fee per kilobyte of transaction.') + ' ' \
2033             + _('Recommended value') + ': ' + self.format_amount(50000)
2034         grid_wallet.addWidget(HelpButton(msg), 0, 3)
2035         if not self.config.is_modifiable('fee_per_kb'):
2036             for w in [fee_e, fee_label]: w.setEnabled(False)
2037
2038         usechange_cb = QCheckBox(_('Use change addresses'))
2039         usechange_cb.setChecked(self.wallet.use_change)
2040         grid_wallet.addWidget(usechange_cb, 1, 0)
2041         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
2042         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2043
2044         gap_label = QLabel(_('Gap limit'))
2045         grid_wallet.addWidget(gap_label, 2, 0)
2046         gap_e = AmountEdit(None,True)
2047         gap_e.setText("%d"% self.wallet.gap_limit)
2048         grid_wallet.addWidget(gap_e, 2, 2)
2049         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
2050               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
2051               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
2052               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
2053               + _('Warning') + ': ' \
2054               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
2055               + _('Do not modify it if you do not understand what you are doing, or if you expect to recover your wallet without knowing it!') + '\n\n' 
2056         grid_wallet.addWidget(HelpButton(msg), 2, 3)
2057         if not self.config.is_modifiable('gap_limit'):
2058             for w in [gap_e, gap_label]: w.setEnabled(False)
2059
2060         units = ['BTC', 'mBTC']
2061         unit_label = QLabel(_('Base unit'))
2062         grid_wallet.addWidget(unit_label, 3, 0)
2063         unit_combo = QComboBox()
2064         unit_combo.addItems(units)
2065         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2066         grid_wallet.addWidget(unit_combo, 3, 2)
2067         grid_wallet.addWidget(HelpButton(_('Base unit of your wallet.')\
2068                                              + '\n1BTC=1000mBTC.\n' \
2069                                              + _(' This settings affects the fields in the Send tab')+' '), 3, 3)
2070         grid_wallet.setRowStretch(4,1)
2071
2072         # plugins
2073         if self.plugins:
2074             tab5 = QScrollArea()
2075             tab5.setEnabled(True)
2076             tab5.setWidgetResizable(True)
2077
2078             grid_plugins = QGridLayout()
2079             grid_plugins.setColumnStretch(0,1)
2080
2081             w = QWidget()
2082             w.setLayout(grid_plugins)
2083             tab5.setWidget(w)
2084
2085             w.setMinimumHeight(len(self.plugins)*35)
2086
2087             tabs.addTab(tab5, _('Plugins') )
2088             def mk_toggle(cb, p):
2089                 return lambda: cb.setChecked(p.toggle())
2090             for i, p in enumerate(self.plugins):
2091                 try:
2092                     cb = QCheckBox(p.fullname())
2093                     cb.setDisabled(not p.is_available())
2094                     cb.setChecked(p.is_enabled())
2095                     cb.clicked.connect(mk_toggle(cb,p))
2096                     grid_plugins.addWidget(cb, i, 0)
2097                     if p.requires_settings():
2098                         grid_plugins.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2099                     grid_plugins.addWidget(HelpButton(p.description()), i, 2)
2100                 except:
2101                     print_msg("Error: cannot display plugin", p)
2102                     traceback.print_exc(file=sys.stdout)
2103             grid_plugins.setRowStretch(i+1,1)
2104
2105         self.run_hook('create_settings_tab', tabs)
2106
2107         vbox.addLayout(ok_cancel_buttons(d))
2108         d.setLayout(vbox) 
2109
2110         # run the dialog
2111         if not d.exec_(): return
2112
2113         fee = unicode(fee_e.text())
2114         try:
2115             fee = self.read_amount(fee)
2116         except:
2117             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2118             return
2119
2120         self.set_fee(fee)
2121         
2122         nz = unicode(nz_e.text())
2123         try:
2124             nz = int( nz )
2125             if nz>8: nz=8
2126         except:
2127             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2128             return
2129
2130         if self.num_zeros != nz:
2131             self.num_zeros = nz
2132             self.config.set_key('num_zeros', nz, True)
2133             self.update_history_tab()
2134             self.update_receive_tab()
2135
2136         usechange_result = usechange_cb.isChecked()
2137         if self.wallet.use_change != usechange_result:
2138             self.wallet.use_change = usechange_result
2139             self.config.set_key('use_change', self.wallet.use_change, True)
2140         
2141         unit_result = units[unit_combo.currentIndex()]
2142         if self.base_unit() != unit_result:
2143             self.decimal_point = 8 if unit_result == 'BTC' else 5
2144             self.config.set_key('decimal_point', self.decimal_point, True)
2145             self.update_history_tab()
2146             self.update_status()
2147         
2148         try:
2149             n = int(gap_e.text())
2150         except:
2151             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2152             return
2153
2154         if self.wallet.gap_limit != n:
2155             r = self.wallet.change_gap_limit(n)
2156             if r:
2157                 self.update_receive_tab()
2158                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2159             else:
2160                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2161
2162         need_restart = False
2163
2164         lang_request = languages.keys()[lang_combo.currentIndex()]
2165         if lang_request != self.config.get('language'):
2166             self.config.set_key("language", lang_request, True)
2167             need_restart = True
2168             
2169         cur_request = str(currencies[cur_combo.currentIndex()])
2170         if cur_request != self.config.get('currency', "None"):
2171             self.config.set_key('currency', cur_request, True)
2172             self.update_wallet()
2173
2174         self.run_hook('close_settings_dialog')
2175
2176         if need_restart:
2177             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2178
2179         self.receive_tab_set_mode(expert_cb.isChecked())
2180
2181     def run_network_dialog(self):
2182         NetworkDialog(self.wallet.interface, self.config, self).do_exec()
2183
2184     def closeEvent(self, event):
2185         g = self.geometry()
2186         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2187         self.save_column_widths()
2188         self.config.set_key("console-history", self.console.history[-50:], True)
2189         event.accept()
2190
2191 class OpenFileEventFilter(QObject):
2192     def __init__(self, windows):
2193         self.windows = windows
2194         super(OpenFileEventFilter, self).__init__()
2195
2196     def eventFilter(self, obj, event):
2197         if event.type() == QtCore.QEvent.FileOpen:
2198             if len(self.windows) >= 1:
2199                 self.windows[0].set_url(event.url().toString())
2200                 return True
2201         return False
2202
2203
2204
2205
2206 class ElectrumGui:
2207
2208     def __init__(self, config, interface, blockchain, app=None):
2209         self.interface = interface
2210         self.config = config
2211         self.blockchain = blockchain
2212         self.windows = []
2213         self.efilter = OpenFileEventFilter(self.windows)
2214         if app is None:
2215             self.app = QApplication(sys.argv)
2216         self.app.installEventFilter(self.efilter)
2217
2218
2219     def main(self, url):
2220
2221         storage = WalletStorage(self.config)
2222         if not storage.file_exists:
2223             import installwizard
2224             wizard = installwizard.InstallWizard(self.config, self.interface, self.blockchain, storage)
2225             wallet = wizard.run()
2226             if not wallet: 
2227                 exit()
2228         else:
2229             wallet = Wallet(storage)
2230
2231         wallet.start_threads(self.interface, self.blockchain)
2232
2233         s = Timer()
2234         s.start()
2235         w = ElectrumWindow(self.config)
2236         w.load_wallet(wallet)
2237
2238         self.windows.append(w)
2239         if url: w.set_url(url)
2240         w.app = self.app
2241         w.connect_slots(s)
2242         w.update_wallet()
2243         w.show()
2244
2245         self.app.exec_()
2246
2247         wallet.stop_threads()
2248
2249