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