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