Parse amounts from CSV with Decimal and not float.
[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         self.from_label = QLabel(_('From'))
762         grid.addWidget(self.from_label, 3, 0)
763         self.from_list = QTreeWidget(self)
764         self.from_list.setColumnCount(2)
765         self.from_list.setColumnWidth(0, 350)
766         self.from_list.setColumnWidth(1, 50)
767         self.from_list.setHeaderHidden (True)
768         self.from_list.setMaximumHeight(80)
769         grid.addWidget(self.from_list, 3, 1, 1, 3)
770         self.connect(self.tabs, SIGNAL('currentChanged(int)'), 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, 1, 2)
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, 1, 2)
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, fee, 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         self.from_label.setHidden(len(self.pay_from) == 0)
856         self.from_list.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
1022
1023     def create_list_tab(self, headers):
1024         "generic tab creation method"
1025         l = MyTreeWidget(self)
1026         l.setColumnCount( len(headers) )
1027         l.setHeaderLabels( headers )
1028
1029         w = QWidget()
1030         vbox = QVBoxLayout()
1031         w.setLayout(vbox)
1032
1033         vbox.setMargin(0)
1034         vbox.setSpacing(0)
1035         vbox.addWidget(l)
1036         buttons = QWidget()
1037         vbox.addWidget(buttons)
1038
1039         hbox = QHBoxLayout()
1040         hbox.setMargin(0)
1041         hbox.setSpacing(0)
1042         buttons.setLayout(hbox)
1043
1044         return l,w,hbox
1045
1046
1047     def create_receive_tab(self):
1048         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1049         l.setContextMenuPolicy(Qt.CustomContextMenu)
1050         l.customContextMenuRequested.connect(self.create_receive_menu)
1051         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1052         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1053         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1054         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1055         self.receive_list = l
1056         self.receive_buttons_hbox = hbox
1057         hbox.addStretch(1)
1058         return w
1059
1060
1061
1062
1063     def save_column_widths(self):
1064         self.column_widths["receive"] = []
1065         for i in range(self.receive_list.columnCount() -1):
1066             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1067
1068         self.column_widths["history"] = []
1069         for i in range(self.history_list.columnCount() - 1):
1070             self.column_widths["history"].append(self.history_list.columnWidth(i))
1071
1072         self.column_widths["contacts"] = []
1073         for i in range(self.contacts_list.columnCount() - 1):
1074             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1075
1076         self.config.set_key("column_widths_2", self.column_widths, True)
1077
1078
1079     def create_contacts_tab(self):
1080         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1081         l.setContextMenuPolicy(Qt.CustomContextMenu)
1082         l.customContextMenuRequested.connect(self.create_contact_menu)
1083         for i,width in enumerate(self.column_widths['contacts']):
1084             l.setColumnWidth(i, width)
1085
1086         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1087         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1088         self.contacts_list = l
1089         self.contacts_buttons_hbox = hbox
1090         hbox.addStretch(1)
1091         return w
1092
1093
1094     def delete_imported_key(self, addr):
1095         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1096             self.wallet.delete_imported_key(addr)
1097             self.update_receive_tab()
1098             self.update_history_tab()
1099
1100     def edit_account_label(self, k):
1101         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1102         if ok:
1103             label = unicode(text)
1104             self.wallet.set_label(k,label)
1105             self.update_receive_tab()
1106
1107     def account_set_expanded(self, item, k, b):
1108         item.setExpanded(b)
1109         self.accounts_expanded[k] = b
1110
1111     def create_account_menu(self, position, k, item):
1112         menu = QMenu()
1113         if item.isExpanded():
1114             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1115         else:
1116             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1117         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1118         menu.addAction(_("View details"), lambda: self.show_account_details(k))
1119         if self.wallet.account_is_pending(k):
1120             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1121         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1122
1123     def delete_pending_account(self, k):
1124         self.wallet.delete_pending_account(k)
1125         self.update_receive_tab()
1126
1127     def create_receive_menu(self, position):
1128         # fixme: this function apparently has a side effect.
1129         # if it is not called the menu pops up several times
1130         #self.receive_list.selectedIndexes()
1131
1132         selected = self.receive_list.selectedItems()
1133         multi_select = len(selected) > 1
1134         addrs = [unicode(item.text(0)) for item in selected]
1135         if not multi_select:
1136             item = self.receive_list.itemAt(position)
1137             if not item: return
1138
1139             addr = addrs[0]
1140             if not is_valid(addr):
1141                 k = str(item.data(0,32).toString())
1142                 if k:
1143                     self.create_account_menu(position, k, item)
1144                 else:
1145                     item.setExpanded(not item.isExpanded())
1146                 return
1147
1148         menu = QMenu()
1149         if not multi_select:
1150             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1151             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1152             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1153             if self.wallet.seed:
1154                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1155                 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1156             if addr in self.wallet.imported_keys:
1157                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1158
1159         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1160             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1161         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1162             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1163
1164         menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1165
1166         run_hook('receive_menu', menu, addrs)
1167         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1168
1169
1170     def get_sendable_balance(self):
1171         return sum(sum(self.wallet.get_addr_balance(a)) for a in self.get_payment_sources())
1172
1173
1174     def get_payment_sources(self):
1175         if self.pay_from:
1176             return self.pay_from
1177         else:
1178             return self.wallet.get_account_addresses(self.current_account)
1179
1180
1181     def send_from_addresses(self, addrs):
1182         self.pay_from = addrs[:]
1183         self.tabs.setCurrentIndex(1)
1184
1185
1186     def payto(self, addr):
1187         if not addr: return
1188         label = self.wallet.labels.get(addr)
1189         m_addr = label + '  <' + addr + '>' if label else addr
1190         self.tabs.setCurrentIndex(1)
1191         self.payto_e.setText(m_addr)
1192         self.amount_e.setFocus()
1193
1194
1195     def delete_contact(self, x):
1196         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1197             self.wallet.delete_contact(x)
1198             self.wallet.set_label(x, None)
1199             self.update_history_tab()
1200             self.update_contacts_tab()
1201             self.update_completions()
1202
1203
1204     def create_contact_menu(self, position):
1205         item = self.contacts_list.itemAt(position)
1206         if not item: return
1207         addr = unicode(item.text(0))
1208         label = unicode(item.text(1))
1209         is_editable = item.data(0,32).toBool()
1210         payto_addr = item.data(0,33).toString()
1211         menu = QMenu()
1212         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1213         menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1214         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1215         if is_editable:
1216             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1217             menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1218
1219         run_hook('create_contact_menu', menu, item)
1220         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1221
1222
1223     def update_receive_item(self, item):
1224         item.setFont(0, QFont(MONOSPACE_FONT))
1225         address = str(item.data(0,0).toString())
1226         label = self.wallet.labels.get(address,'')
1227         item.setData(1,0,label)
1228         item.setData(0,32, True) # is editable
1229
1230         run_hook('update_receive_item', address, item)
1231
1232         if not self.wallet.is_mine(address): return
1233
1234         c, u = self.wallet.get_addr_balance(address)
1235         balance = self.format_amount(c + u)
1236         item.setData(2,0,balance)
1237
1238         if address in self.wallet.frozen_addresses:
1239             item.setBackgroundColor(0, QColor('lightblue'))
1240
1241
1242     def update_receive_tab(self):
1243         l = self.receive_list
1244
1245         l.clear()
1246         l.setColumnHidden(2, False)
1247         l.setColumnHidden(3, False)
1248         for i,width in enumerate(self.column_widths['receive']):
1249             l.setColumnWidth(i, width)
1250
1251         if self.current_account is None:
1252             account_items = self.wallet.accounts.items()
1253         elif self.current_account != -1:
1254             account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1255         else:
1256             account_items = []
1257
1258         for k, account in account_items:
1259             name = self.wallet.get_account_name(k)
1260             c,u = self.wallet.get_account_balance(k)
1261             account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1262             l.addTopLevelItem(account_item)
1263             account_item.setExpanded(self.accounts_expanded.get(k, True))
1264             account_item.setData(0, 32, k)
1265
1266             if not self.wallet.is_seeded(k):
1267                 icon = QIcon(":icons/key.png")
1268                 account_item.setIcon(0, icon)
1269
1270             for is_change in ([0,1]):
1271                 name = _("Receiving") if not is_change else _("Change")
1272                 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1273                 account_item.addChild(seq_item)
1274                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1275                 used_flag = False
1276                 if not is_change: seq_item.setExpanded(True)
1277
1278                 is_red = False
1279                 gap = 0
1280
1281                 for address in account.get_addresses(is_change):
1282                     h = self.wallet.history.get(address,[])
1283
1284                     if h == []:
1285                         gap += 1
1286                         if gap > self.wallet.gap_limit:
1287                             is_red = True
1288                     else:
1289                         gap = 0
1290
1291                     c, u = self.wallet.get_addr_balance(address)
1292                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1293                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1294                     self.update_receive_item(item)
1295                     if is_red:
1296                         item.setBackgroundColor(1, QColor('red'))
1297                     if len(h) > 0 and c == -u:
1298                         if not used_flag:
1299                             seq_item.addChild(used_item)
1300                             used_flag = True
1301                         used_item.addChild(item)
1302                     else:
1303                         seq_item.addChild(item)
1304
1305
1306         for k, addr in self.wallet.get_pending_accounts():
1307             name = self.wallet.labels.get(k,'')
1308             account_item = QTreeWidgetItem( [ name + "  [ "+_('pending account')+" ]", '', '', ''] )
1309             self.update_receive_item(item)
1310             l.addTopLevelItem(account_item)
1311             account_item.setExpanded(True)
1312             account_item.setData(0, 32, k)
1313             item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1314             account_item.addChild(item)
1315             self.update_receive_item(item)
1316
1317
1318         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1319             c,u = self.wallet.get_imported_balance()
1320             account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1321             l.addTopLevelItem(account_item)
1322             account_item.setExpanded(True)
1323             for address in self.wallet.imported_keys.keys():
1324                 item = QTreeWidgetItem( [ address, '', '', ''] )
1325                 self.update_receive_item(item)
1326                 account_item.addChild(item)
1327
1328
1329         # we use column 1 because column 0 may be hidden
1330         l.setCurrentItem(l.topLevelItem(0),1)
1331
1332
1333     def update_contacts_tab(self):
1334         l = self.contacts_list
1335         l.clear()
1336
1337         for address in self.wallet.addressbook:
1338             label = self.wallet.labels.get(address,'')
1339             n = self.wallet.get_num_tx(address)
1340             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1341             item.setFont(0, QFont(MONOSPACE_FONT))
1342             # 32 = label can be edited (bool)
1343             item.setData(0,32, True)
1344             # 33 = payto string
1345             item.setData(0,33, address)
1346             l.addTopLevelItem(item)
1347
1348         run_hook('update_contacts_tab', l)
1349         l.setCurrentItem(l.topLevelItem(0))
1350
1351
1352
1353     def create_console_tab(self):
1354         from console import Console
1355         self.console = console = Console()
1356         return console
1357
1358
1359     def update_console(self):
1360         console = self.console
1361         console.history = self.config.get("console-history",[])
1362         console.history_index = len(console.history)
1363
1364         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1365         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1366
1367         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1368         methods = {}
1369         def mkfunc(f, method):
1370             return lambda *args: apply( f, (method, args, self.password_dialog ))
1371         for m in dir(c):
1372             if m[0]=='_' or m in ['network','wallet']: continue
1373             methods[m] = mkfunc(c._run, m)
1374
1375         console.updateNamespace(methods)
1376
1377
1378     def change_account(self,s):
1379         if s == _("All accounts"):
1380             self.current_account = None
1381         else:
1382             accounts = self.wallet.get_account_names()
1383             for k, v in accounts.items():
1384                 if v == s:
1385                     self.current_account = k
1386         self.update_history_tab()
1387         self.update_status()
1388         self.update_receive_tab()
1389
1390     def create_status_bar(self):
1391
1392         sb = QStatusBar()
1393         sb.setFixedHeight(35)
1394         qtVersion = qVersion()
1395
1396         self.balance_label = QLabel("")
1397         sb.addWidget(self.balance_label)
1398
1399         from version_getter import UpdateLabel
1400         self.updatelabel = UpdateLabel(self.config, sb)
1401
1402         self.account_selector = QComboBox()
1403         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1404         sb.addPermanentWidget(self.account_selector)
1405
1406         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1407             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1408
1409         self.lock_icon = QIcon()
1410         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1411         sb.addPermanentWidget( self.password_button )
1412
1413         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1414         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1415         sb.addPermanentWidget( self.seed_button )
1416         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1417         sb.addPermanentWidget( self.status_button )
1418
1419         run_hook('create_status_bar', (sb,))
1420
1421         self.setStatusBar(sb)
1422
1423
1424     def update_lock_icon(self):
1425         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1426         self.password_button.setIcon( icon )
1427
1428
1429     def update_buttons_on_seed(self):
1430         if not self.wallet.is_watching_only():
1431            self.seed_button.show()
1432            self.password_button.show()
1433            self.send_button.setText(_("Send"))
1434         else:
1435            self.password_button.hide()
1436            self.seed_button.hide()
1437            self.send_button.setText(_("Create unsigned transaction"))
1438
1439
1440     def change_password_dialog(self):
1441         from password_dialog import PasswordDialog
1442         d = PasswordDialog(self.wallet, self)
1443         d.run()
1444         self.update_lock_icon()
1445
1446
1447     def new_contact_dialog(self):
1448
1449         d = QDialog(self)
1450         vbox = QVBoxLayout(d)
1451         vbox.addWidget(QLabel(_('New Contact')+':'))
1452
1453         grid = QGridLayout()
1454         line1 = QLineEdit()
1455         line2 = QLineEdit()
1456         grid.addWidget(QLabel(_("Address")), 1, 0)
1457         grid.addWidget(line1, 1, 1)
1458         grid.addWidget(QLabel(_("Name")), 2, 0)
1459         grid.addWidget(line2, 2, 1)
1460
1461         vbox.addLayout(grid)
1462         vbox.addLayout(ok_cancel_buttons(d))
1463
1464         if not d.exec_():
1465             return
1466
1467         address = str(line1.text())
1468         label = unicode(line2.text())
1469
1470         if not is_valid(address):
1471             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1472             return
1473
1474         self.wallet.add_contact(address)
1475         if label:
1476             self.wallet.set_label(address, label)
1477
1478         self.update_contacts_tab()
1479         self.update_history_tab()
1480         self.update_completions()
1481         self.tabs.setCurrentIndex(3)
1482
1483
1484     def new_account_dialog(self):
1485
1486         dialog = QDialog(self)
1487         dialog.setModal(1)
1488         dialog.setWindowTitle(_("New Account"))
1489
1490         vbox = QVBoxLayout()
1491         vbox.addWidget(QLabel(_('Account name')+':'))
1492         e = QLineEdit()
1493         vbox.addWidget(e)
1494         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1495             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1496         l = QLabel(msg)
1497         l.setWordWrap(True)
1498         vbox.addWidget(l)
1499
1500         vbox.addLayout(ok_cancel_buttons(dialog))
1501         dialog.setLayout(vbox)
1502         r = dialog.exec_()
1503         if not r: return
1504
1505         name = str(e.text())
1506         if not name: return
1507
1508         self.wallet.create_pending_account('1', name)
1509         self.update_receive_tab()
1510         self.tabs.setCurrentIndex(2)
1511
1512
1513
1514     def show_master_public_key_old(self):
1515         dialog = QDialog(self)
1516         dialog.setModal(1)
1517         dialog.setWindowTitle(_("Master Public Key"))
1518
1519         main_text = QTextEdit()
1520         main_text.setText(self.wallet.get_master_public_key())
1521         main_text.setReadOnly(True)
1522         main_text.setMaximumHeight(170)
1523         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1524
1525         ok_button = QPushButton(_("OK"))
1526         ok_button.setDefault(True)
1527         ok_button.clicked.connect(dialog.accept)
1528
1529         main_layout = QGridLayout()
1530         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1531
1532         main_layout.addWidget(main_text, 1, 0)
1533         main_layout.addWidget(qrw, 1, 1 )
1534
1535         vbox = QVBoxLayout()
1536         vbox.addLayout(main_layout)
1537         vbox.addLayout(close_button(dialog))
1538         dialog.setLayout(vbox)
1539         dialog.exec_()
1540
1541
1542     def show_master_public_key(self):
1543
1544         if self.wallet.seed_version == 4:
1545             self.show_master_public_key_old()
1546             return
1547
1548         dialog = QDialog(self)
1549         dialog.setModal(1)
1550         dialog.setWindowTitle(_("Master Public Keys"))
1551
1552         chain_text = QTextEdit()
1553         chain_text.setReadOnly(True)
1554         chain_text.setMaximumHeight(170)
1555         chain_qrw = QRCodeWidget()
1556
1557         mpk_text = QTextEdit()
1558         mpk_text.setReadOnly(True)
1559         mpk_text.setMaximumHeight(170)
1560         mpk_qrw = QRCodeWidget()
1561
1562         main_layout = QGridLayout()
1563
1564         main_layout.addWidget(QLabel(_('Key')), 1, 0)
1565         main_layout.addWidget(mpk_text, 1, 1)
1566         main_layout.addWidget(mpk_qrw, 1, 2)
1567
1568         main_layout.addWidget(QLabel(_('Chain')), 2, 0)
1569         main_layout.addWidget(chain_text, 2, 1)
1570         main_layout.addWidget(chain_qrw, 2, 2)
1571
1572         def update(key):
1573             c, K, cK = self.wallet.master_public_keys[str(key)]
1574             chain_text.setText(c)
1575             chain_qrw.set_addr(c)
1576             chain_qrw.update_qr()
1577             mpk_text.setText(K)
1578             mpk_qrw.set_addr(K)
1579             mpk_qrw.update_qr()
1580
1581         key_selector = QComboBox()
1582         keys = sorted(self.wallet.master_public_keys.keys())
1583         key_selector.addItems(keys)
1584
1585         main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1586         main_layout.addWidget(key_selector, 0, 1)
1587         dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1588
1589         update(keys[0])
1590
1591         vbox = QVBoxLayout()
1592         vbox.addLayout(main_layout)
1593         vbox.addLayout(close_button(dialog))
1594
1595         dialog.setLayout(vbox)
1596         dialog.exec_()
1597
1598
1599     @protected
1600     def show_seed_dialog(self, password):
1601         if self.wallet.is_watching_only():
1602             QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1603             return
1604
1605         if self.wallet.seed:
1606             try:
1607                 mnemonic = self.wallet.get_mnemonic(password)
1608             except Exception:
1609                 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1610                 return
1611             from seed_dialog import SeedDialog
1612             d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1613             d.exec_()
1614         else:
1615             l = {}
1616             for k in self.wallet.master_private_keys.keys():
1617                 pk = self.wallet.get_master_private_key(k, password)
1618                 l[k] = pk
1619             from seed_dialog import PrivateKeysDialog
1620             d = PrivateKeysDialog(self,l)
1621             d.exec_()
1622
1623
1624
1625
1626
1627     def show_qrcode(self, data, title = _("QR code")):
1628         if not data: return
1629         d = QDialog(self)
1630         d.setModal(1)
1631         d.setWindowTitle(title)
1632         d.setMinimumSize(270, 300)
1633         vbox = QVBoxLayout()
1634         qrw = QRCodeWidget(data)
1635         vbox.addWidget(qrw, 1)
1636         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1637         hbox = QHBoxLayout()
1638         hbox.addStretch(1)
1639
1640         filename = os.path.join(self.config.path, "qrcode.bmp")
1641
1642         def print_qr():
1643             bmp.save_qrcode(qrw.qr, filename)
1644             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1645
1646         def copy_to_clipboard():
1647             bmp.save_qrcode(qrw.qr, filename)
1648             self.app.clipboard().setImage(QImage(filename))
1649             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1650
1651         b = QPushButton(_("Copy"))
1652         hbox.addWidget(b)
1653         b.clicked.connect(copy_to_clipboard)
1654
1655         b = QPushButton(_("Save"))
1656         hbox.addWidget(b)
1657         b.clicked.connect(print_qr)
1658
1659         b = QPushButton(_("Close"))
1660         hbox.addWidget(b)
1661         b.clicked.connect(d.accept)
1662         b.setDefault(True)
1663
1664         vbox.addLayout(hbox)
1665         d.setLayout(vbox)
1666         d.exec_()
1667
1668
1669     def do_protect(self, func, args):
1670         if self.wallet.use_encryption:
1671             password = self.password_dialog()
1672             if not password:
1673                 return
1674         else:
1675             password = None
1676
1677         if args != (False,):
1678             args = (self,) + args + (password,)
1679         else:
1680             args = (self,password)
1681         apply( func, args)
1682
1683
1684     @protected
1685     def show_private_key(self, address, password):
1686         if not address: return
1687         try:
1688             pk_list = self.wallet.get_private_key(address, password)
1689         except Exception as e:
1690             self.show_message(str(e))
1691             return
1692         QMessageBox.information(self, _('Private key'), _('Address')+ ': ' + address + '\n\n' + _('Private key') + ': ' + '\n'.join(pk_list), _('OK'))
1693
1694
1695     @protected
1696     def do_sign(self, address, message, signature, password):
1697         message = unicode(message.toPlainText())
1698         message = message.encode('utf-8')
1699         try:
1700             sig = self.wallet.sign_message(str(address.text()), message, password)
1701             signature.setText(sig)
1702         except Exception as e:
1703             self.show_message(str(e))
1704
1705     def sign_message(self, address):
1706         if not address: return
1707         d = QDialog(self)
1708         d.setModal(1)
1709         d.setWindowTitle(_('Sign Message'))
1710         d.setMinimumSize(410, 290)
1711
1712         tab_widget = QTabWidget()
1713         tab = QWidget()
1714         layout = QGridLayout(tab)
1715
1716         sign_address = QLineEdit()
1717
1718         sign_address.setText(address)
1719         layout.addWidget(QLabel(_('Address')), 1, 0)
1720         layout.addWidget(sign_address, 1, 1)
1721
1722         sign_message = QTextEdit()
1723         layout.addWidget(QLabel(_('Message')), 2, 0)
1724         layout.addWidget(sign_message, 2, 1)
1725         layout.setRowStretch(2,3)
1726
1727         sign_signature = QTextEdit()
1728         layout.addWidget(QLabel(_('Signature')), 3, 0)
1729         layout.addWidget(sign_signature, 3, 1)
1730         layout.setRowStretch(3,1)
1731
1732
1733         hbox = QHBoxLayout()
1734         b = QPushButton(_("Sign"))
1735         hbox.addWidget(b)
1736         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1737         b = QPushButton(_("Close"))
1738         b.clicked.connect(d.accept)
1739         hbox.addWidget(b)
1740         layout.addLayout(hbox, 4, 1)
1741         tab_widget.addTab(tab, _("Sign"))
1742
1743
1744         tab = QWidget()
1745         layout = QGridLayout(tab)
1746
1747         verify_address = QLineEdit()
1748         layout.addWidget(QLabel(_('Address')), 1, 0)
1749         layout.addWidget(verify_address, 1, 1)
1750
1751         verify_message = QTextEdit()
1752         layout.addWidget(QLabel(_('Message')), 2, 0)
1753         layout.addWidget(verify_message, 2, 1)
1754         layout.setRowStretch(2,3)
1755
1756         verify_signature = QTextEdit()
1757         layout.addWidget(QLabel(_('Signature')), 3, 0)
1758         layout.addWidget(verify_signature, 3, 1)
1759         layout.setRowStretch(3,1)
1760
1761         def do_verify():
1762             message = unicode(verify_message.toPlainText())
1763             message = message.encode('utf-8')
1764             if bitcoin.verify_message(verify_address.text(), str(verify_signature.toPlainText()), message):
1765                 self.show_message(_("Signature verified"))
1766             else:
1767                 self.show_message(_("Error: wrong signature"))
1768
1769         hbox = QHBoxLayout()
1770         b = QPushButton(_("Verify"))
1771         b.clicked.connect(do_verify)
1772         hbox.addWidget(b)
1773         b = QPushButton(_("Close"))
1774         b.clicked.connect(d.accept)
1775         hbox.addWidget(b)
1776         layout.addLayout(hbox, 4, 1)
1777         tab_widget.addTab(tab, _("Verify"))
1778
1779         vbox = QVBoxLayout()
1780         vbox.addWidget(tab_widget)
1781         d.setLayout(vbox)
1782         d.exec_()
1783
1784
1785
1786
1787     def question(self, msg):
1788         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1789
1790     def show_message(self, msg):
1791         QMessageBox.information(self, _('Message'), msg, _('OK'))
1792
1793     def password_dialog(self ):
1794         d = QDialog(self)
1795         d.setModal(1)
1796
1797         pw = QLineEdit()
1798         pw.setEchoMode(2)
1799
1800         vbox = QVBoxLayout()
1801         msg = _('Please enter your password')
1802         vbox.addWidget(QLabel(msg))
1803
1804         grid = QGridLayout()
1805         grid.setSpacing(8)
1806         grid.addWidget(QLabel(_('Password')), 1, 0)
1807         grid.addWidget(pw, 1, 1)
1808         vbox.addLayout(grid)
1809
1810         vbox.addLayout(ok_cancel_buttons(d))
1811         d.setLayout(vbox)
1812
1813         run_hook('password_dialog', pw, grid, 1)
1814         if not d.exec_(): return
1815         return unicode(pw.text())
1816
1817
1818
1819
1820
1821
1822
1823
1824     def tx_from_text(self, txt):
1825         "json or raw hexadecimal"
1826         try:
1827             txt.decode('hex')
1828             tx = Transaction(txt)
1829             return tx
1830         except Exception:
1831             pass
1832
1833         try:
1834             tx_dict = json.loads(str(txt))
1835             assert "hex" in tx_dict.keys()
1836             assert "complete" in tx_dict.keys()
1837             tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1838             if not tx_dict["complete"]:
1839                 assert "input_info" in tx_dict.keys()
1840                 input_info = json.loads(tx_dict['input_info'])
1841                 tx.add_input_info(input_info)
1842             return tx
1843         except Exception:
1844             pass
1845
1846         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1847
1848
1849
1850     def read_tx_from_file(self):
1851         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1852         if not fileName:
1853             return
1854         try:
1855             with open(fileName, "r") as f:
1856                 file_content = f.read()
1857         except (ValueError, IOError, os.error), reason:
1858             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1859
1860         return self.tx_from_text(file_content)
1861
1862
1863     @protected
1864     def sign_raw_transaction(self, tx, input_info, password):
1865         self.wallet.signrawtransaction(tx, input_info, [], password)
1866
1867     def do_process_from_text(self):
1868         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1869         if not text:
1870             return
1871         tx = self.tx_from_text(text)
1872         if tx:
1873             self.show_transaction(tx)
1874
1875     def do_process_from_file(self):
1876         tx = self.read_tx_from_file()
1877         if tx:
1878             self.show_transaction(tx)
1879
1880     def do_process_from_csvReader(self, csvReader):
1881         outputs = []
1882         try:
1883             for row in csvReader:
1884                 address = row[0]
1885                 amount = Decimal(row[1])
1886                 amount = int(100000000*amount)
1887                 outputs.append((address, amount))
1888         except (ValueError, IOError, os.error), reason:
1889             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1890             return
1891
1892         try:
1893             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1894         except Exception as e:
1895             self.show_message(str(e))
1896             return
1897
1898         self.show_transaction(tx)
1899
1900     def do_process_from_csv_file(self):
1901         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1902         if not fileName:
1903             return
1904         try:
1905             with open(fileName, "r") as f:
1906                 csvReader = csv.reader(f)
1907                 self.do_process_from_csvReader(csvReader)
1908         except (ValueError, IOError, os.error), reason:
1909             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1910             return
1911
1912     def do_process_from_csv_text(self):
1913         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1914                                + _("Format: address, amount. One output per line"), _("Load CSV"))
1915         if not text:
1916             return
1917         f = StringIO.StringIO(text)
1918         csvReader = csv.reader(f)
1919         self.do_process_from_csvReader(csvReader)
1920
1921
1922
1923     @protected
1924     def do_export_privkeys(self, password):
1925         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.")))
1926
1927         try:
1928             select_export = _('Select file to export your private keys to')
1929             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1930             if fileName:
1931                 with open(fileName, "w+") as csvfile:
1932                     transaction = csv.writer(csvfile)
1933                     transaction.writerow(["address", "private_key"])
1934
1935                     addresses = self.wallet.addresses(True)
1936
1937                     for addr in addresses:
1938                         pk = "".join(self.wallet.get_private_key(addr, password))
1939                         transaction.writerow(["%34s"%addr,pk])
1940
1941                     self.show_message(_("Private keys exported."))
1942
1943         except (IOError, os.error), reason:
1944             export_error_label = _("Electrum was unable to produce a private key-export.")
1945             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1946
1947         except Exception as e:
1948           self.show_message(str(e))
1949           return
1950
1951
1952     def do_import_labels(self):
1953         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1954         if not labelsFile: return
1955         try:
1956             f = open(labelsFile, 'r')
1957             data = f.read()
1958             f.close()
1959             for key, value in json.loads(data).items():
1960                 self.wallet.set_label(key, value)
1961             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1962         except (IOError, os.error), reason:
1963             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1964
1965
1966     def do_export_labels(self):
1967         labels = self.wallet.labels
1968         try:
1969             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1970             if fileName:
1971                 with open(fileName, 'w+') as f:
1972                     json.dump(labels, f)
1973                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1974         except (IOError, os.error), reason:
1975             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1976
1977
1978     def do_export_history(self):
1979         from lite_window import csv_transaction
1980         csv_transaction(self.wallet)
1981
1982
1983     @protected
1984     def do_import_privkey(self, password):
1985         if not self.wallet.imported_keys:
1986             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1987                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1988                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1989             if r == 4: return
1990
1991         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1992         if not text: return
1993
1994         text = str(text).split()
1995         badkeys = []
1996         addrlist = []
1997         for key in text:
1998             try:
1999                 addr = self.wallet.import_key(key, password)
2000             except Exception as e:
2001                 badkeys.append(key)
2002                 continue
2003             if not addr:
2004                 badkeys.append(key)
2005             else:
2006                 addrlist.append(addr)
2007         if addrlist:
2008             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2009         if badkeys:
2010             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2011         self.update_receive_tab()
2012         self.update_history_tab()
2013
2014
2015     def settings_dialog(self):
2016         d = QDialog(self)
2017         d.setWindowTitle(_('Electrum Settings'))
2018         d.setModal(1)
2019         vbox = QVBoxLayout()
2020         grid = QGridLayout()
2021         grid.setColumnStretch(0,1)
2022
2023         nz_label = QLabel(_('Display zeros') + ':')
2024         grid.addWidget(nz_label, 0, 0)
2025         nz_e = AmountEdit(None,True)
2026         nz_e.setText("%d"% self.num_zeros)
2027         grid.addWidget(nz_e, 0, 1)
2028         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2029         grid.addWidget(HelpButton(msg), 0, 2)
2030         if not self.config.is_modifiable('num_zeros'):
2031             for w in [nz_e, nz_label]: w.setEnabled(False)
2032
2033         lang_label=QLabel(_('Language') + ':')
2034         grid.addWidget(lang_label, 1, 0)
2035         lang_combo = QComboBox()
2036         from electrum.i18n import languages
2037         lang_combo.addItems(languages.values())
2038         try:
2039             index = languages.keys().index(self.config.get("language",''))
2040         except Exception:
2041             index = 0
2042         lang_combo.setCurrentIndex(index)
2043         grid.addWidget(lang_combo, 1, 1)
2044         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2045         if not self.config.is_modifiable('language'):
2046             for w in [lang_combo, lang_label]: w.setEnabled(False)
2047
2048
2049         fee_label = QLabel(_('Transaction fee') + ':')
2050         grid.addWidget(fee_label, 2, 0)
2051         fee_e = AmountEdit(self.base_unit)
2052         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2053         grid.addWidget(fee_e, 2, 1)
2054         msg = _('Fee per kilobyte of transaction.') + ' ' \
2055             + _('Recommended value') + ': ' + self.format_amount(20000)
2056         grid.addWidget(HelpButton(msg), 2, 2)
2057         if not self.config.is_modifiable('fee_per_kb'):
2058             for w in [fee_e, fee_label]: w.setEnabled(False)
2059
2060         units = ['BTC', 'mBTC']
2061         unit_label = QLabel(_('Base unit') + ':')
2062         grid.addWidget(unit_label, 3, 0)
2063         unit_combo = QComboBox()
2064         unit_combo.addItems(units)
2065         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2066         grid.addWidget(unit_combo, 3, 1)
2067         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2068                                              + '\n1BTC=1000mBTC.\n' \
2069                                              + _(' This settings affects the fields in the Send tab')+' '), 3, 2)
2070
2071         usechange_cb = QCheckBox(_('Use change addresses'))
2072         usechange_cb.setChecked(self.wallet.use_change)
2073         grid.addWidget(usechange_cb, 4, 0)
2074         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2075         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2076
2077         grid.setRowStretch(5,1)
2078
2079         vbox.addLayout(grid)
2080         vbox.addLayout(ok_cancel_buttons(d))
2081         d.setLayout(vbox)
2082
2083         # run the dialog
2084         if not d.exec_(): return
2085
2086         fee = unicode(fee_e.text())
2087         try:
2088             fee = self.read_amount(fee)
2089         except Exception:
2090             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2091             return
2092
2093         self.wallet.set_fee(fee)
2094
2095         nz = unicode(nz_e.text())
2096         try:
2097             nz = int( nz )
2098             if nz>8: nz=8
2099         except Exception:
2100             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2101             return
2102
2103         if self.num_zeros != nz:
2104             self.num_zeros = nz
2105             self.config.set_key('num_zeros', nz, True)
2106             self.update_history_tab()
2107             self.update_receive_tab()
2108
2109         usechange_result = usechange_cb.isChecked()
2110         if self.wallet.use_change != usechange_result:
2111             self.wallet.use_change = usechange_result
2112             self.wallet.storage.put('use_change', self.wallet.use_change)
2113
2114         unit_result = units[unit_combo.currentIndex()]
2115         if self.base_unit() != unit_result:
2116             self.decimal_point = 8 if unit_result == 'BTC' else 5
2117             self.config.set_key('decimal_point', self.decimal_point, True)
2118             self.update_history_tab()
2119             self.update_status()
2120
2121         need_restart = False
2122
2123         lang_request = languages.keys()[lang_combo.currentIndex()]
2124         if lang_request != self.config.get('language'):
2125             self.config.set_key("language", lang_request, True)
2126             need_restart = True
2127
2128         run_hook('close_settings_dialog')
2129
2130         if need_restart:
2131             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2132
2133
2134     def run_network_dialog(self):
2135         if not self.network:
2136             return
2137         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2138
2139     def closeEvent(self, event):
2140         self.tray.hide()
2141         g = self.geometry()
2142         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2143         self.save_column_widths()
2144         self.config.set_key("console-history", self.console.history[-50:], True)
2145         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2146         event.accept()
2147
2148
2149
2150     def plugins_dialog(self):
2151         from electrum.plugins import plugins
2152
2153         d = QDialog(self)
2154         d.setWindowTitle(_('Electrum Plugins'))
2155         d.setModal(1)
2156
2157         vbox = QVBoxLayout(d)
2158
2159         # plugins
2160         scroll = QScrollArea()
2161         scroll.setEnabled(True)
2162         scroll.setWidgetResizable(True)
2163         scroll.setMinimumSize(400,250)
2164         vbox.addWidget(scroll)
2165
2166         w = QWidget()
2167         scroll.setWidget(w)
2168         w.setMinimumHeight(len(plugins)*35)
2169
2170         grid = QGridLayout()
2171         grid.setColumnStretch(0,1)
2172         w.setLayout(grid)
2173
2174         def do_toggle(cb, p, w):
2175             r = p.toggle()
2176             cb.setChecked(r)
2177             if w: w.setEnabled(r)
2178
2179         def mk_toggle(cb, p, w):
2180             return lambda: do_toggle(cb,p,w)
2181
2182         for i, p in enumerate(plugins):
2183             try:
2184                 cb = QCheckBox(p.fullname())
2185                 cb.setDisabled(not p.is_available())
2186                 cb.setChecked(p.is_enabled())
2187                 grid.addWidget(cb, i, 0)
2188                 if p.requires_settings():
2189                     w = p.settings_widget(self)
2190                     w.setEnabled( p.is_enabled() )
2191                     grid.addWidget(w, i, 1)
2192                 else:
2193                     w = None
2194                 cb.clicked.connect(mk_toggle(cb,p,w))
2195                 grid.addWidget(HelpButton(p.description()), i, 2)
2196             except Exception:
2197                 print_msg(_("Error: cannot display plugin"), p)
2198                 traceback.print_exc(file=sys.stdout)
2199         grid.setRowStretch(i+1,1)
2200
2201         vbox.addLayout(close_button(d))
2202
2203         d.exec_()
2204
2205
2206     def show_account_details(self, k):
2207         d = QDialog(self)
2208         d.setWindowTitle(_('Account Details'))
2209         d.setModal(1)
2210
2211         vbox = QVBoxLayout(d)
2212         roots = self.wallet.get_roots(k)
2213
2214         name = self.wallet.get_account_name(k)
2215         label = QLabel('Name: ' + name)
2216         vbox.addWidget(label)
2217
2218         acctype = '2 of 2' if len(roots) == 2 else '2 of 3' if len(roots) == 3 else 'Single key'
2219         vbox.addWidget(QLabel('Type: ' + acctype))
2220
2221         label = QLabel('Derivation: ' + k)
2222         vbox.addWidget(label)
2223
2224         #for root in roots:
2225         #    mpk = self.wallet.master_public_keys[root]
2226         #    text = QTextEdit()
2227         #    text.setReadOnly(True)
2228         #    text.setMaximumHeight(120)
2229         #    text.setText(repr(mpk))
2230         #    vbox.addWidget(text)
2231
2232         vbox.addLayout(close_button(d))
2233         d.exec_()