cleanup wallet classes
[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
884     def set_send(self, address, amount, label, message):
885
886         if label and self.wallet.labels.get(address) != label:
887             if self.question('Give label "%s" to address %s ?'%(label,address)):
888                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
889                     self.wallet.addressbook.append(address)
890                 self.wallet.set_label(address, label)
891
892         self.tabs.setCurrentIndex(1)
893         label = self.wallet.labels.get(address)
894         m_addr = label + '  <'+ address +'>' if label else address
895         self.payto_e.setText(m_addr)
896
897         self.message_e.setText(message)
898         if amount:
899             self.amount_e.setText(amount)
900
901
902     def do_clear(self):
903         self.payto_sig.setVisible(False)
904         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
905             e.setText('')
906             self.set_frozen(e,False)
907
908         self.set_pay_from([])
909         self.update_status()
910
911     def set_frozen(self,entry,frozen):
912         if frozen:
913             entry.setReadOnly(True)
914             entry.setFrame(False)
915             palette = QPalette()
916             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
917             entry.setPalette(palette)
918         else:
919             entry.setReadOnly(False)
920             entry.setFrame(True)
921             palette = QPalette()
922             palette.setColor(entry.backgroundRole(), QColor('white'))
923             entry.setPalette(palette)
924
925
926     def set_addrs_frozen(self,addrs,freeze):
927         for addr in addrs:
928             if not addr: continue
929             if addr in self.wallet.frozen_addresses and not freeze:
930                 self.wallet.unfreeze(addr)
931             elif addr not in self.wallet.frozen_addresses and freeze:
932                 self.wallet.freeze(addr)
933         self.update_receive_tab()
934
935
936
937     def create_list_tab(self, headers):
938         "generic tab creation method"
939         l = MyTreeWidget(self)
940         l.setColumnCount( len(headers) )
941         l.setHeaderLabels( headers )
942
943         w = QWidget()
944         vbox = QVBoxLayout()
945         w.setLayout(vbox)
946
947         vbox.setMargin(0)
948         vbox.setSpacing(0)
949         vbox.addWidget(l)
950         buttons = QWidget()
951         vbox.addWidget(buttons)
952
953         hbox = QHBoxLayout()
954         hbox.setMargin(0)
955         hbox.setSpacing(0)
956         buttons.setLayout(hbox)
957
958         return l,w,hbox
959
960
961     def create_receive_tab(self):
962         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
963         l.setContextMenuPolicy(Qt.CustomContextMenu)
964         l.customContextMenuRequested.connect(self.create_receive_menu)
965         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
966         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
967         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
968         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
969         self.receive_list = l
970         self.receive_buttons_hbox = hbox
971         hbox.addStretch(1)
972         return w
973
974
975
976
977     def save_column_widths(self):
978         self.column_widths["receive"] = []
979         for i in range(self.receive_list.columnCount() -1):
980             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
981
982         self.column_widths["history"] = []
983         for i in range(self.history_list.columnCount() - 1):
984             self.column_widths["history"].append(self.history_list.columnWidth(i))
985
986         self.column_widths["contacts"] = []
987         for i in range(self.contacts_list.columnCount() - 1):
988             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
989
990         self.config.set_key("column_widths_2", self.column_widths, True)
991
992
993     def create_contacts_tab(self):
994         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
995         l.setContextMenuPolicy(Qt.CustomContextMenu)
996         l.customContextMenuRequested.connect(self.create_contact_menu)
997         for i,width in enumerate(self.column_widths['contacts']):
998             l.setColumnWidth(i, width)
999
1000         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1001         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1002         self.contacts_list = l
1003         self.contacts_buttons_hbox = hbox
1004         hbox.addStretch(1)
1005         return w
1006
1007
1008     def delete_imported_key(self, addr):
1009         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1010             self.wallet.delete_imported_key(addr)
1011             self.update_receive_tab()
1012             self.update_history_tab()
1013
1014     def edit_account_label(self, k):
1015         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1016         if ok:
1017             label = unicode(text)
1018             self.wallet.set_label(k,label)
1019             self.update_receive_tab()
1020
1021     def account_set_expanded(self, item, k, b):
1022         item.setExpanded(b)
1023         self.accounts_expanded[k] = b
1024
1025     def create_account_menu(self, position, k, item):
1026         menu = QMenu()
1027         if item.isExpanded():
1028             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1029         else:
1030             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1031         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1032         if self.wallet.seed_version > 4:
1033             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1034         if self.wallet.account_is_pending(k):
1035             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1036         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1037
1038     def delete_pending_account(self, k):
1039         self.wallet.delete_pending_account(k)
1040         self.update_receive_tab()
1041
1042     def create_receive_menu(self, position):
1043         # fixme: this function apparently has a side effect.
1044         # if it is not called the menu pops up several times
1045         #self.receive_list.selectedIndexes()
1046
1047         selected = self.receive_list.selectedItems()
1048         multi_select = len(selected) > 1
1049         addrs = [unicode(item.text(0)) for item in selected]
1050         if not multi_select:
1051             item = self.receive_list.itemAt(position)
1052             if not item: return
1053
1054             addr = addrs[0]
1055             if not is_valid(addr):
1056                 k = str(item.data(0,32).toString())
1057                 if k:
1058                     self.create_account_menu(position, k, item)
1059                 else:
1060                     item.setExpanded(not item.isExpanded())
1061                 return
1062
1063         menu = QMenu()
1064         if not multi_select:
1065             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1066             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1067             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1068             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1069             if self.wallet.seed:
1070                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1071                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1072                 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1073             if addr in self.wallet.imported_keys:
1074                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1075
1076         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1077             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1078         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1079             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1080
1081         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1082             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1083
1084         run_hook('receive_menu', menu, addrs)
1085         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1086
1087
1088     def get_sendable_balance(self):
1089         return sum(sum(self.wallet.get_addr_balance(a)) for a in self.get_payment_sources())
1090
1091
1092     def get_payment_sources(self):
1093         if self.pay_from:
1094             return self.pay_from
1095         else:
1096             return self.wallet.get_account_addresses(self.current_account)
1097
1098
1099     def send_from_addresses(self, addrs):
1100         self.set_pay_from( addrs )
1101         self.tabs.setCurrentIndex(1)
1102
1103
1104     def payto(self, addr):
1105         if not addr: return
1106         label = self.wallet.labels.get(addr)
1107         m_addr = label + '  <' + addr + '>' if label else addr
1108         self.tabs.setCurrentIndex(1)
1109         self.payto_e.setText(m_addr)
1110         self.amount_e.setFocus()
1111
1112
1113     def delete_contact(self, x):
1114         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1115             self.wallet.delete_contact(x)
1116             self.wallet.set_label(x, None)
1117             self.update_history_tab()
1118             self.update_contacts_tab()
1119             self.update_completions()
1120
1121
1122     def create_contact_menu(self, position):
1123         item = self.contacts_list.itemAt(position)
1124         menu = QMenu()
1125         if not item:
1126             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1127         else:
1128             addr = unicode(item.text(0))
1129             label = unicode(item.text(1))
1130             is_editable = item.data(0,32).toBool()
1131             payto_addr = item.data(0,33).toString()
1132             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1133             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1134             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1135             if is_editable:
1136                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1137                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1138
1139         run_hook('create_contact_menu', menu, item)
1140         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1141
1142
1143     def update_receive_item(self, item):
1144         item.setFont(0, QFont(MONOSPACE_FONT))
1145         address = str(item.data(0,0).toString())
1146         label = self.wallet.labels.get(address,'')
1147         item.setData(1,0,label)
1148         item.setData(0,32, True) # is editable
1149
1150         run_hook('update_receive_item', address, item)
1151
1152         if not self.wallet.is_mine(address): return
1153
1154         c, u = self.wallet.get_addr_balance(address)
1155         balance = self.format_amount(c + u)
1156         item.setData(2,0,balance)
1157
1158         if address in self.wallet.frozen_addresses:
1159             item.setBackgroundColor(0, QColor('lightblue'))
1160
1161
1162     def update_receive_tab(self):
1163         l = self.receive_list
1164
1165         l.clear()
1166         l.setColumnHidden(2, False)
1167         l.setColumnHidden(3, False)
1168         for i,width in enumerate(self.column_widths['receive']):
1169             l.setColumnWidth(i, width)
1170
1171         if self.current_account is None:
1172             account_items = sorted(self.wallet.accounts.items())
1173         elif self.current_account != -1:
1174             account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1175         else:
1176             account_items = []
1177
1178         pending_accounts = self.wallet.get_pending_accounts()
1179
1180         for k, account in account_items:
1181
1182             if len(account_items) + len(pending_accounts) > 1:
1183                 name = self.wallet.get_account_name(k)
1184                 c,u = self.wallet.get_account_balance(k)
1185                 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1186                 l.addTopLevelItem(account_item)
1187                 account_item.setExpanded(self.accounts_expanded.get(k, True))
1188                 account_item.setData(0, 32, k)
1189             else:
1190                 account_item = None
1191
1192             for is_change in ([0,1]):
1193                 name = _("Receiving") if not is_change else _("Change")
1194                 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1195                 if account_item:
1196                     account_item.addChild(seq_item)
1197                 else:
1198                     l.addTopLevelItem(seq_item)
1199                     
1200                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1201                 used_flag = False
1202                 if not is_change: seq_item.setExpanded(True)
1203
1204                 is_red = False
1205                 gap = 0
1206
1207                 for address in account.get_addresses(is_change):
1208                     h = self.wallet.history.get(address,[])
1209
1210                     if h == []:
1211                         gap += 1
1212                         if gap > self.wallet.gap_limit:
1213                             is_red = True
1214                     else:
1215                         gap = 0
1216
1217                     c, u = self.wallet.get_addr_balance(address)
1218                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1219                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1220                     self.update_receive_item(item)
1221                     if is_red:
1222                         item.setBackgroundColor(1, QColor('red'))
1223                     if len(h) > 0 and c == -u:
1224                         if not used_flag:
1225                             seq_item.insertChild(0,used_item)
1226                             used_flag = True
1227                         used_item.addChild(item)
1228                     else:
1229                         seq_item.addChild(item)
1230
1231
1232         for k, addr in pending_accounts:
1233             name = self.wallet.labels.get(k,'')
1234             account_item = QTreeWidgetItem( [ name + "  [ "+_('pending account')+" ]", '', '', ''] )
1235             self.update_receive_item(item)
1236             l.addTopLevelItem(account_item)
1237             account_item.setExpanded(True)
1238             account_item.setData(0, 32, k)
1239             item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1240             account_item.addChild(item)
1241             self.update_receive_item(item)
1242
1243
1244         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1245             c,u = self.wallet.get_imported_balance()
1246             account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1247             l.addTopLevelItem(account_item)
1248             account_item.setExpanded(True)
1249             for address in self.wallet.imported_keys.keys():
1250                 item = QTreeWidgetItem( [ address, '', '', ''] )
1251                 self.update_receive_item(item)
1252                 account_item.addChild(item)
1253
1254
1255         # we use column 1 because column 0 may be hidden
1256         l.setCurrentItem(l.topLevelItem(0),1)
1257
1258
1259     def update_contacts_tab(self):
1260         l = self.contacts_list
1261         l.clear()
1262
1263         for address in self.wallet.addressbook:
1264             label = self.wallet.labels.get(address,'')
1265             n = self.wallet.get_num_tx(address)
1266             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1267             item.setFont(0, QFont(MONOSPACE_FONT))
1268             # 32 = label can be edited (bool)
1269             item.setData(0,32, True)
1270             # 33 = payto string
1271             item.setData(0,33, address)
1272             l.addTopLevelItem(item)
1273
1274         run_hook('update_contacts_tab', l)
1275         l.setCurrentItem(l.topLevelItem(0))
1276
1277
1278
1279     def create_console_tab(self):
1280         from console import Console
1281         self.console = console = Console()
1282         return console
1283
1284
1285     def update_console(self):
1286         console = self.console
1287         console.history = self.config.get("console-history",[])
1288         console.history_index = len(console.history)
1289
1290         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1291         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1292
1293         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1294         methods = {}
1295         def mkfunc(f, method):
1296             return lambda *args: apply( f, (method, args, self.password_dialog ))
1297         for m in dir(c):
1298             if m[0]=='_' or m in ['network','wallet']: continue
1299             methods[m] = mkfunc(c._run, m)
1300
1301         console.updateNamespace(methods)
1302
1303
1304     def change_account(self,s):
1305         if s == _("All accounts"):
1306             self.current_account = None
1307         else:
1308             accounts = self.wallet.get_account_names()
1309             for k, v in accounts.items():
1310                 if v == s:
1311                     self.current_account = k
1312         self.update_history_tab()
1313         self.update_status()
1314         self.update_receive_tab()
1315
1316     def create_status_bar(self):
1317
1318         sb = QStatusBar()
1319         sb.setFixedHeight(35)
1320         qtVersion = qVersion()
1321
1322         self.balance_label = QLabel("")
1323         sb.addWidget(self.balance_label)
1324
1325         from version_getter import UpdateLabel
1326         self.updatelabel = UpdateLabel(self.config, sb)
1327
1328         self.account_selector = QComboBox()
1329         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1330         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1331         sb.addPermanentWidget(self.account_selector)
1332
1333         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1334             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1335
1336         self.lock_icon = QIcon()
1337         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1338         sb.addPermanentWidget( self.password_button )
1339
1340         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1341         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1342         sb.addPermanentWidget( self.seed_button )
1343         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1344         sb.addPermanentWidget( self.status_button )
1345
1346         run_hook('create_status_bar', (sb,))
1347
1348         self.setStatusBar(sb)
1349
1350
1351     def update_lock_icon(self):
1352         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1353         self.password_button.setIcon( icon )
1354
1355
1356     def update_buttons_on_seed(self):
1357         if not self.wallet.is_watching_only():
1358            self.seed_button.show()
1359            self.password_button.show()
1360            self.send_button.setText(_("Send"))
1361         else:
1362            self.password_button.hide()
1363            self.seed_button.hide()
1364            self.send_button.setText(_("Create unsigned transaction"))
1365
1366
1367     def change_password_dialog(self):
1368         from password_dialog import PasswordDialog
1369         d = PasswordDialog(self.wallet, self)
1370         d.run()
1371         self.update_lock_icon()
1372
1373
1374     def new_contact_dialog(self):
1375
1376         d = QDialog(self)
1377         d.setWindowTitle(_("New Contact"))
1378         vbox = QVBoxLayout(d)
1379         vbox.addWidget(QLabel(_('New Contact')+':'))
1380
1381         grid = QGridLayout()
1382         line1 = QLineEdit()
1383         line2 = QLineEdit()
1384         grid.addWidget(QLabel(_("Address")), 1, 0)
1385         grid.addWidget(line1, 1, 1)
1386         grid.addWidget(QLabel(_("Name")), 2, 0)
1387         grid.addWidget(line2, 2, 1)
1388
1389         vbox.addLayout(grid)
1390         vbox.addLayout(ok_cancel_buttons(d))
1391
1392         if not d.exec_():
1393             return
1394
1395         address = str(line1.text())
1396         label = unicode(line2.text())
1397
1398         if not is_valid(address):
1399             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1400             return
1401
1402         self.wallet.add_contact(address)
1403         if label:
1404             self.wallet.set_label(address, label)
1405
1406         self.update_contacts_tab()
1407         self.update_history_tab()
1408         self.update_completions()
1409         self.tabs.setCurrentIndex(3)
1410
1411
1412     @protected
1413     def new_account_dialog(self, password):
1414
1415         dialog = QDialog(self)
1416         dialog.setModal(1)
1417         dialog.setWindowTitle(_("New Account"))
1418
1419         vbox = QVBoxLayout()
1420         vbox.addWidget(QLabel(_('Account name')+':'))
1421         e = QLineEdit()
1422         vbox.addWidget(e)
1423         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1424             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1425         l = QLabel(msg)
1426         l.setWordWrap(True)
1427         vbox.addWidget(l)
1428
1429         vbox.addLayout(ok_cancel_buttons(dialog))
1430         dialog.setLayout(vbox)
1431         r = dialog.exec_()
1432         if not r: return
1433
1434         name = str(e.text())
1435         if not name: return
1436
1437         self.wallet.create_pending_account(name, password)
1438         self.update_receive_tab()
1439         self.tabs.setCurrentIndex(2)
1440
1441
1442
1443
1444     def show_master_public_keys(self):
1445
1446         dialog = QDialog(self)
1447         dialog.setModal(1)
1448         dialog.setWindowTitle(_("Master Public Keys"))
1449
1450         main_layout = QGridLayout()
1451         mpk_dict = self.wallet.get_master_public_keys()
1452         i = 0
1453         for key, value in mpk_dict.items():
1454             main_layout.addWidget(QLabel(key), i, 0)
1455             mpk_text = QTextEdit()
1456             mpk_text.setReadOnly(True)
1457             mpk_text.setMaximumHeight(170)
1458             mpk_text.setText(value)
1459             main_layout.addWidget(mpk_text, i + 1, 0)
1460             i += 2
1461
1462         vbox = QVBoxLayout()
1463         vbox.addLayout(main_layout)
1464         vbox.addLayout(close_button(dialog))
1465
1466         dialog.setLayout(vbox)
1467         dialog.exec_()
1468
1469
1470     @protected
1471     def show_seed_dialog(self, password):
1472         if self.wallet.is_watching_only():
1473             QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1474             return
1475
1476         if self.wallet.seed:
1477             try:
1478                 mnemonic = self.wallet.get_mnemonic(password)
1479             except Exception:
1480                 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1481                 return
1482             from seed_dialog import SeedDialog
1483             d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1484             d.exec_()
1485         else:
1486             l = {}
1487             for k in self.wallet.master_private_keys.keys():
1488                 pk = self.wallet.get_master_private_key(k, password)
1489                 l[k] = pk
1490             from seed_dialog import PrivateKeysDialog
1491             d = PrivateKeysDialog(self,l)
1492             d.exec_()
1493
1494
1495
1496
1497
1498     def show_qrcode(self, data, title = _("QR code")):
1499         if not data: return
1500         d = QDialog(self)
1501         d.setModal(1)
1502         d.setWindowTitle(title)
1503         d.setMinimumSize(270, 300)
1504         vbox = QVBoxLayout()
1505         qrw = QRCodeWidget(data)
1506         vbox.addWidget(qrw, 1)
1507         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1508         hbox = QHBoxLayout()
1509         hbox.addStretch(1)
1510
1511         filename = os.path.join(self.config.path, "qrcode.bmp")
1512
1513         def print_qr():
1514             bmp.save_qrcode(qrw.qr, filename)
1515             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1516
1517         def copy_to_clipboard():
1518             bmp.save_qrcode(qrw.qr, filename)
1519             self.app.clipboard().setImage(QImage(filename))
1520             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1521
1522         b = QPushButton(_("Copy"))
1523         hbox.addWidget(b)
1524         b.clicked.connect(copy_to_clipboard)
1525
1526         b = QPushButton(_("Save"))
1527         hbox.addWidget(b)
1528         b.clicked.connect(print_qr)
1529
1530         b = QPushButton(_("Close"))
1531         hbox.addWidget(b)
1532         b.clicked.connect(d.accept)
1533         b.setDefault(True)
1534
1535         vbox.addLayout(hbox)
1536         d.setLayout(vbox)
1537         d.exec_()
1538
1539
1540     def do_protect(self, func, args):
1541         if self.wallet.use_encryption:
1542             password = self.password_dialog()
1543             if not password:
1544                 return
1545         else:
1546             password = None
1547
1548         if args != (False,):
1549             args = (self,) + args + (password,)
1550         else:
1551             args = (self,password)
1552         apply( func, args)
1553
1554
1555     def show_public_keys(self, address):
1556         if not address: return
1557         try:
1558             pubkey_list = self.wallet.get_public_keys(address)
1559         except Exception as e:
1560             traceback.print_exc(file=sys.stdout)
1561             self.show_message(str(e))
1562             return
1563
1564         d = QDialog(self)
1565         d.setMinimumSize(600, 200)
1566         d.setModal(1)
1567         vbox = QVBoxLayout()
1568         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1569         vbox.addWidget( QLabel(_("Public key") + ':'))
1570         keys = QTextEdit()
1571         keys.setReadOnly(True)
1572         keys.setText('\n'.join(pubkey_list))
1573         vbox.addWidget(keys)
1574         #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1575         vbox.addLayout(close_button(d))
1576         d.setLayout(vbox)
1577         d.exec_()
1578
1579     @protected
1580     def show_private_key(self, address, password):
1581         if not address: return
1582         try:
1583             pk_list = self.wallet.get_private_key(address, password)
1584         except Exception as e:
1585             traceback.print_exc(file=sys.stdout)
1586             self.show_message(str(e))
1587             return
1588
1589         d = QDialog(self)
1590         d.setMinimumSize(600, 200)
1591         d.setModal(1)
1592         vbox = QVBoxLayout()
1593         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1594         vbox.addWidget( QLabel(_("Private key") + ':'))
1595         keys = QTextEdit()
1596         keys.setReadOnly(True)
1597         keys.setText('\n'.join(pk_list))
1598         vbox.addWidget(keys)
1599         vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1600         vbox.addLayout(close_button(d))
1601         d.setLayout(vbox)
1602         d.exec_()
1603
1604
1605     @protected
1606     def do_sign(self, address, message, signature, password):
1607         message = unicode(message.toPlainText())
1608         message = message.encode('utf-8')
1609         try:
1610             sig = self.wallet.sign_message(str(address.text()), message, password)
1611             signature.setText(sig)
1612         except Exception as e:
1613             self.show_message(str(e))
1614
1615     def do_verify(self, address, message, signature):
1616         message = unicode(message.toPlainText())
1617         message = message.encode('utf-8')
1618         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1619             self.show_message(_("Signature verified"))
1620         else:
1621             self.show_message(_("Error: wrong signature"))
1622
1623
1624     def sign_verify_message(self, address=''):
1625         d = QDialog(self)
1626         d.setModal(1)
1627         d.setWindowTitle(_('Sign/verify Message'))
1628         d.setMinimumSize(410, 290)
1629
1630         layout = QGridLayout(d)
1631
1632         message_e = QTextEdit()
1633         layout.addWidget(QLabel(_('Message')), 1, 0)
1634         layout.addWidget(message_e, 1, 1)
1635         layout.setRowStretch(2,3)
1636
1637         address_e = QLineEdit()
1638         address_e.setText(address)
1639         layout.addWidget(QLabel(_('Address')), 2, 0)
1640         layout.addWidget(address_e, 2, 1)
1641
1642         signature_e = QTextEdit()
1643         layout.addWidget(QLabel(_('Signature')), 3, 0)
1644         layout.addWidget(signature_e, 3, 1)
1645         layout.setRowStretch(3,1)
1646
1647         hbox = QHBoxLayout()
1648
1649         b = QPushButton(_("Sign"))
1650         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1651         hbox.addWidget(b)
1652
1653         b = QPushButton(_("Verify"))
1654         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1655         hbox.addWidget(b)
1656
1657         b = QPushButton(_("Close"))
1658         b.clicked.connect(d.accept)
1659         hbox.addWidget(b)
1660         layout.addLayout(hbox, 4, 1)
1661         d.exec_()
1662
1663
1664     @protected
1665     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1666         try:
1667             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1668             message_e.setText(decrypted)
1669         except Exception as e:
1670             self.show_message(str(e))
1671
1672
1673     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1674         message = unicode(message_e.toPlainText())
1675         message = message.encode('utf-8')
1676         try:
1677             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1678             encrypted_e.setText(encrypted)
1679         except Exception as e:
1680             self.show_message(str(e))
1681
1682
1683
1684     def encrypt_message(self, address = ''):
1685         d = QDialog(self)
1686         d.setModal(1)
1687         d.setWindowTitle(_('Encrypt/decrypt Message'))
1688         d.setMinimumSize(610, 490)
1689
1690         layout = QGridLayout(d)
1691
1692         message_e = QTextEdit()
1693         layout.addWidget(QLabel(_('Message')), 1, 0)
1694         layout.addWidget(message_e, 1, 1)
1695         layout.setRowStretch(2,3)
1696
1697         pubkey_e = QLineEdit()
1698         if address:
1699             pubkey = self.wallet.getpubkeys(address)[0]
1700             pubkey_e.setText(pubkey)
1701         layout.addWidget(QLabel(_('Public key')), 2, 0)
1702         layout.addWidget(pubkey_e, 2, 1)
1703
1704         encrypted_e = QTextEdit()
1705         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1706         layout.addWidget(encrypted_e, 3, 1)
1707         layout.setRowStretch(3,1)
1708
1709         hbox = QHBoxLayout()
1710         b = QPushButton(_("Encrypt"))
1711         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1712         hbox.addWidget(b)
1713
1714         b = QPushButton(_("Decrypt"))
1715         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1716         hbox.addWidget(b)
1717
1718         b = QPushButton(_("Close"))
1719         b.clicked.connect(d.accept)
1720         hbox.addWidget(b)
1721
1722         layout.addLayout(hbox, 4, 1)
1723         d.exec_()
1724
1725
1726     def question(self, msg):
1727         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1728
1729     def show_message(self, msg):
1730         QMessageBox.information(self, _('Message'), msg, _('OK'))
1731
1732     def password_dialog(self ):
1733         d = QDialog(self)
1734         d.setModal(1)
1735         d.setWindowTitle(_("Enter Password"))
1736
1737         pw = QLineEdit()
1738         pw.setEchoMode(2)
1739
1740         vbox = QVBoxLayout()
1741         msg = _('Please enter your password')
1742         vbox.addWidget(QLabel(msg))
1743
1744         grid = QGridLayout()
1745         grid.setSpacing(8)
1746         grid.addWidget(QLabel(_('Password')), 1, 0)
1747         grid.addWidget(pw, 1, 1)
1748         vbox.addLayout(grid)
1749
1750         vbox.addLayout(ok_cancel_buttons(d))
1751         d.setLayout(vbox)
1752
1753         run_hook('password_dialog', pw, grid, 1)
1754         if not d.exec_(): return
1755         return unicode(pw.text())
1756
1757
1758
1759
1760
1761
1762
1763
1764     def tx_from_text(self, txt):
1765         "json or raw hexadecimal"
1766         try:
1767             txt.decode('hex')
1768             tx = Transaction(txt)
1769             return tx
1770         except Exception:
1771             pass
1772
1773         try:
1774             tx_dict = json.loads(str(txt))
1775             assert "hex" in tx_dict.keys()
1776             assert "complete" in tx_dict.keys()
1777             tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1778             if not tx_dict["complete"]:
1779                 assert "input_info" in tx_dict.keys()
1780                 input_info = json.loads(tx_dict['input_info'])
1781                 tx.add_input_info(input_info)
1782             return tx
1783         except Exception:
1784             pass
1785
1786         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1787
1788
1789
1790     def read_tx_from_file(self):
1791         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1792         if not fileName:
1793             return
1794         try:
1795             with open(fileName, "r") as f:
1796                 file_content = f.read()
1797         except (ValueError, IOError, os.error), reason:
1798             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1799
1800         return self.tx_from_text(file_content)
1801
1802
1803     @protected
1804     def sign_raw_transaction(self, tx, input_info, password):
1805         self.wallet.signrawtransaction(tx, input_info, [], password)
1806
1807     def do_process_from_text(self):
1808         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1809         if not text:
1810             return
1811         tx = self.tx_from_text(text)
1812         if tx:
1813             self.show_transaction(tx)
1814
1815     def do_process_from_file(self):
1816         tx = self.read_tx_from_file()
1817         if tx:
1818             self.show_transaction(tx)
1819
1820     def do_process_from_txid(self):
1821         from electrum import transaction
1822         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1823         if ok and txid:
1824             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1825             if r:
1826                 tx = transaction.Transaction(r)
1827                 if tx:
1828                     self.show_transaction(tx)
1829                 else:
1830                     self.show_message("unknown transaction")
1831
1832     def do_process_from_csvReader(self, csvReader):
1833         outputs = []
1834         errors = []
1835         errtext = ""
1836         try:
1837             for position, row in enumerate(csvReader):
1838                 address = row[0]
1839                 if not is_valid(address):
1840                     errors.append((position, address))
1841                     continue
1842                 amount = Decimal(row[1])
1843                 amount = int(100000000*amount)
1844                 outputs.append((address, amount))
1845         except (ValueError, IOError, os.error), reason:
1846             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1847             return
1848         if errors != []:
1849             for x in errors:
1850                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1851             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1852             return
1853
1854         try:
1855             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1856         except Exception as e:
1857             self.show_message(str(e))
1858             return
1859
1860         self.show_transaction(tx)
1861
1862     def do_process_from_csv_file(self):
1863         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1864         if not fileName:
1865             return
1866         try:
1867             with open(fileName, "r") as f:
1868                 csvReader = csv.reader(f)
1869                 self.do_process_from_csvReader(csvReader)
1870         except (ValueError, IOError, os.error), reason:
1871             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1872             return
1873
1874     def do_process_from_csv_text(self):
1875         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1876                                + _("Format: address, amount. One output per line"), _("Load CSV"))
1877         if not text:
1878             return
1879         f = StringIO.StringIO(text)
1880         csvReader = csv.reader(f)
1881         self.do_process_from_csvReader(csvReader)
1882
1883
1884
1885     @protected
1886     def do_export_privkeys(self, password):
1887         if not self.wallet.seed:
1888             self.show_message(_("This wallet has no seed"))
1889             return
1890
1891         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.")))
1892
1893         try:
1894             select_export = _('Select file to export your private keys to')
1895             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1896             if fileName:
1897                 with open(fileName, "w+") as csvfile:
1898                     transaction = csv.writer(csvfile)
1899                     transaction.writerow(["address", "private_key"])
1900
1901                     addresses = self.wallet.addresses(True)
1902
1903                     for addr in addresses:
1904                         pk = "".join(self.wallet.get_private_key(addr, password))
1905                         transaction.writerow(["%34s"%addr,pk])
1906
1907                     self.show_message(_("Private keys exported."))
1908
1909         except (IOError, os.error), reason:
1910             export_error_label = _("Electrum was unable to produce a private key-export.")
1911             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1912
1913         except Exception as e:
1914           self.show_message(str(e))
1915           return
1916
1917
1918     def do_import_labels(self):
1919         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1920         if not labelsFile: return
1921         try:
1922             f = open(labelsFile, 'r')
1923             data = f.read()
1924             f.close()
1925             for key, value in json.loads(data).items():
1926                 self.wallet.set_label(key, value)
1927             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1928         except (IOError, os.error), reason:
1929             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1930
1931
1932     def do_export_labels(self):
1933         labels = self.wallet.labels
1934         try:
1935             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1936             if fileName:
1937                 with open(fileName, 'w+') as f:
1938                     json.dump(labels, f)
1939                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1940         except (IOError, os.error), reason:
1941             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
1942
1943
1944     def do_export_history(self):
1945         wallet = self.wallet
1946         select_export = _('Select file to export your wallet transactions to')
1947         fileName = QFileDialog.getSaveFileName(QWidget(), select_export, os.path.expanduser('~/electrum-history.csv'), "*.csv")
1948         if not fileName:
1949             return
1950
1951         try:
1952             with open(fileName, "w+") as csvfile:
1953                 transaction = csv.writer(csvfile)
1954                 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
1955                 for item in wallet.get_tx_history():
1956                     tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
1957                     if confirmations:
1958                         if timestamp is not None:
1959                             try:
1960                                 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1961                             except [RuntimeError, TypeError, NameError] as reason:
1962                                 time_string = "unknown"
1963                                 pass
1964                         else:
1965                           time_string = "unknown"
1966                     else:
1967                         time_string = "pending"
1968
1969                     if value is not None:
1970                         value_string = format_satoshis(value, True)
1971                     else:
1972                         value_string = '--'
1973
1974                     if fee is not None:
1975                         fee_string = format_satoshis(fee, True)
1976                     else:
1977                         fee_string = '0'
1978
1979                     if tx_hash:
1980                         label, is_default_label = wallet.get_label(tx_hash)
1981                         label = label.encode('utf-8')
1982                     else:
1983                       label = ""
1984
1985                     balance_string = format_satoshis(balance, False)
1986                     transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
1987                 QMessageBox.information(None,_("CSV Export created"), _("Your CSV export has been successfully created."))
1988
1989         except (IOError, os.error), reason:
1990             export_error_label = _("Electrum was unable to produce a transaction export.")
1991             QMessageBox.critical(None,_("Unable to create csv"), export_error_label + "\n" + str(reason))
1992
1993
1994
1995     @protected
1996     def do_import_privkey(self, password):
1997         if not self.wallet.imported_keys:
1998             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1999                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2000                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2001             if r == 4: return
2002
2003         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2004         if not text: return
2005
2006         text = str(text).split()
2007         badkeys = []
2008         addrlist = []
2009         for key in text:
2010             try:
2011                 addr = self.wallet.import_key(key, password)
2012             except Exception as e:
2013                 badkeys.append(key)
2014                 continue
2015             if not addr:
2016                 badkeys.append(key)
2017             else:
2018                 addrlist.append(addr)
2019         if addrlist:
2020             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2021         if badkeys:
2022             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2023         self.update_receive_tab()
2024         self.update_history_tab()
2025
2026
2027     def settings_dialog(self):
2028         d = QDialog(self)
2029         d.setWindowTitle(_('Electrum Settings'))
2030         d.setModal(1)
2031         vbox = QVBoxLayout()
2032         grid = QGridLayout()
2033         grid.setColumnStretch(0,1)
2034
2035         nz_label = QLabel(_('Display zeros') + ':')
2036         grid.addWidget(nz_label, 0, 0)
2037         nz_e = AmountEdit(None,True)
2038         nz_e.setText("%d"% self.num_zeros)
2039         grid.addWidget(nz_e, 0, 1)
2040         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2041         grid.addWidget(HelpButton(msg), 0, 2)
2042         if not self.config.is_modifiable('num_zeros'):
2043             for w in [nz_e, nz_label]: w.setEnabled(False)
2044
2045         lang_label=QLabel(_('Language') + ':')
2046         grid.addWidget(lang_label, 1, 0)
2047         lang_combo = QComboBox()
2048         from electrum.i18n import languages
2049         lang_combo.addItems(languages.values())
2050         try:
2051             index = languages.keys().index(self.config.get("language",''))
2052         except Exception:
2053             index = 0
2054         lang_combo.setCurrentIndex(index)
2055         grid.addWidget(lang_combo, 1, 1)
2056         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2057         if not self.config.is_modifiable('language'):
2058             for w in [lang_combo, lang_label]: w.setEnabled(False)
2059
2060
2061         fee_label = QLabel(_('Transaction fee') + ':')
2062         grid.addWidget(fee_label, 2, 0)
2063         fee_e = AmountEdit(self.base_unit)
2064         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2065         grid.addWidget(fee_e, 2, 1)
2066         msg = _('Fee per kilobyte of transaction.') + ' ' \
2067             + _('Recommended value') + ': ' + self.format_amount(20000)
2068         grid.addWidget(HelpButton(msg), 2, 2)
2069         if not self.config.is_modifiable('fee_per_kb'):
2070             for w in [fee_e, fee_label]: w.setEnabled(False)
2071
2072         units = ['BTC', 'mBTC']
2073         unit_label = QLabel(_('Base unit') + ':')
2074         grid.addWidget(unit_label, 3, 0)
2075         unit_combo = QComboBox()
2076         unit_combo.addItems(units)
2077         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2078         grid.addWidget(unit_combo, 3, 1)
2079         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2080                                              + '\n1BTC=1000mBTC.\n' \
2081                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2082
2083         usechange_cb = QCheckBox(_('Use change addresses'))
2084         usechange_cb.setChecked(self.wallet.use_change)
2085         grid.addWidget(usechange_cb, 4, 0)
2086         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2087         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2088
2089         block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2090         block_ex_label = QLabel(_('Online Block Explorer') + ':')
2091         grid.addWidget(block_ex_label, 5, 0)
2092         block_ex_combo = QComboBox()
2093         block_ex_combo.addItems(block_explorers)
2094         block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2095         grid.addWidget(block_ex_combo, 5, 1)
2096         grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2097
2098         show_tx = self.config.get('show_before_broadcast', False)
2099         showtx_cb = QCheckBox(_('Show before broadcast'))
2100         showtx_cb.setChecked(show_tx)
2101         grid.addWidget(showtx_cb, 6, 0)
2102         grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2103
2104         vbox.addLayout(grid)
2105         vbox.addStretch(1)
2106         vbox.addLayout(ok_cancel_buttons(d))
2107         d.setLayout(vbox)
2108
2109         # run the dialog
2110         if not d.exec_(): return
2111
2112         fee = unicode(fee_e.text())
2113         try:
2114             fee = self.read_amount(fee)
2115         except Exception:
2116             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2117             return
2118
2119         self.wallet.set_fee(fee)
2120
2121         nz = unicode(nz_e.text())
2122         try:
2123             nz = int( nz )
2124             if nz>8: nz=8
2125         except Exception:
2126             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2127             return
2128
2129         if self.num_zeros != nz:
2130             self.num_zeros = nz
2131             self.config.set_key('num_zeros', nz, True)
2132             self.update_history_tab()
2133             self.update_receive_tab()
2134
2135         usechange_result = usechange_cb.isChecked()
2136         if self.wallet.use_change != usechange_result:
2137             self.wallet.use_change = usechange_result
2138             self.wallet.storage.put('use_change', self.wallet.use_change)
2139
2140         if showtx_cb.isChecked() != show_tx:
2141             self.config.set_key('show_before_broadcast', not show_tx)
2142
2143         unit_result = units[unit_combo.currentIndex()]
2144         if self.base_unit() != unit_result:
2145             self.decimal_point = 8 if unit_result == 'BTC' else 5
2146             self.config.set_key('decimal_point', self.decimal_point, True)
2147             self.update_history_tab()
2148             self.update_status()
2149
2150         need_restart = False
2151
2152         lang_request = languages.keys()[lang_combo.currentIndex()]
2153         if lang_request != self.config.get('language'):
2154             self.config.set_key("language", lang_request, True)
2155             need_restart = True
2156
2157         be_result = block_explorers[block_ex_combo.currentIndex()]
2158         self.config.set_key('block_explorer', be_result, True)
2159
2160         run_hook('close_settings_dialog')
2161
2162         if need_restart:
2163             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2164
2165
2166     def run_network_dialog(self):
2167         if not self.network:
2168             return
2169         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2170
2171     def closeEvent(self, event):
2172         self.tray.hide()
2173         g = self.geometry()
2174         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2175         self.save_column_widths()
2176         self.config.set_key("console-history", self.console.history[-50:], True)
2177         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2178         event.accept()
2179
2180
2181     def plugins_dialog(self):
2182         from electrum.plugins import plugins
2183
2184         d = QDialog(self)
2185         d.setWindowTitle(_('Electrum Plugins'))
2186         d.setModal(1)
2187
2188         vbox = QVBoxLayout(d)
2189
2190         # plugins
2191         scroll = QScrollArea()
2192         scroll.setEnabled(True)
2193         scroll.setWidgetResizable(True)
2194         scroll.setMinimumSize(400,250)
2195         vbox.addWidget(scroll)
2196
2197         w = QWidget()
2198         scroll.setWidget(w)
2199         w.setMinimumHeight(len(plugins)*35)
2200
2201         grid = QGridLayout()
2202         grid.setColumnStretch(0,1)
2203         w.setLayout(grid)
2204
2205         def do_toggle(cb, p, w):
2206             r = p.toggle()
2207             cb.setChecked(r)
2208             if w: w.setEnabled(r)
2209
2210         def mk_toggle(cb, p, w):
2211             return lambda: do_toggle(cb,p,w)
2212
2213         for i, p in enumerate(plugins):
2214             try:
2215                 cb = QCheckBox(p.fullname())
2216                 cb.setDisabled(not p.is_available())
2217                 cb.setChecked(p.is_enabled())
2218                 grid.addWidget(cb, i, 0)
2219                 if p.requires_settings():
2220                     w = p.settings_widget(self)
2221                     w.setEnabled( p.is_enabled() )
2222                     grid.addWidget(w, i, 1)
2223                 else:
2224                     w = None
2225                 cb.clicked.connect(mk_toggle(cb,p,w))
2226                 grid.addWidget(HelpButton(p.description()), i, 2)
2227             except Exception:
2228                 print_msg(_("Error: cannot display plugin"), p)
2229                 traceback.print_exc(file=sys.stdout)
2230         grid.setRowStretch(i+1,1)
2231
2232         vbox.addLayout(close_button(d))
2233
2234         d.exec_()
2235
2236
2237     def show_account_details(self, k):
2238         account = self.wallet.accounts[k]
2239
2240         d = QDialog(self)
2241         d.setWindowTitle(_('Account Details'))
2242         d.setModal(1)
2243
2244         vbox = QVBoxLayout(d)
2245         name = self.wallet.get_account_name(k)
2246         label = QLabel('Name: ' + name)
2247         vbox.addWidget(label)
2248
2249         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2250
2251         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2252
2253         vbox.addWidget(QLabel(_('Master Public Key:')))
2254
2255         text = QTextEdit()
2256         text.setReadOnly(True)
2257         text.setMaximumHeight(170)
2258         vbox.addWidget(text)
2259
2260         mpk_text = '\n'.join( account.get_master_pubkeys() )
2261         text.setText(mpk_text)
2262
2263         vbox.addLayout(close_button(d))
2264         d.exec_()