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