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