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