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