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