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