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