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