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