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