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