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