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