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