paytoedit
[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         from paytoedit import PayToEdit
647         self.payto_e = PayToEdit()
648         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)'))
649         grid.addWidget(QLabel(_('Pay to')), 1, 0)
650         grid.addWidget(self.payto_e, 1, 1, 1, 3)
651         grid.addWidget(self.payto_help, 1, 4)
652
653         completer = QCompleter()
654         completer.setCaseSensitivity(False)
655         self.payto_e.setCompleter(completer)
656         completer.setModel(self.completions)
657
658         self.message_e = QLineEdit()
659         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.'))
660         grid.addWidget(QLabel(_('Description')), 2, 0)
661         grid.addWidget(self.message_e, 2, 1, 1, 3)
662         grid.addWidget(self.message_help, 2, 4)
663
664         self.from_label = QLabel(_('From'))
665         grid.addWidget(self.from_label, 3, 0)
666         self.from_list = QTreeWidget(self)
667         self.from_list.setColumnCount(2)
668         self.from_list.setColumnWidth(0, 350)
669         self.from_list.setColumnWidth(1, 50)
670         self.from_list.setHeaderHidden (True)
671         self.from_list.setMaximumHeight(80)
672         grid.addWidget(self.from_list, 3, 1, 1, 3)
673         self.set_pay_from([])
674
675         self.amount_e = AmountEdit(self.get_decimal_point)
676         self.amount_help = HelpButton(_('Amount to be sent.') + '\n\n' \
677                                       + _('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.') \
678                                       + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.'))
679         grid.addWidget(QLabel(_('Amount')), 4, 0)
680         grid.addWidget(self.amount_e, 4, 1, 1, 2)
681         grid.addWidget(self.amount_help, 4, 3)
682
683         self.fee_e = AmountEdit(self.get_decimal_point)
684         grid.addWidget(QLabel(_('Fee')), 5, 0)
685         grid.addWidget(self.fee_e, 5, 1, 1, 2)
686         grid.addWidget(HelpButton(
687                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
688                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
689                     + _('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)
690
691         run_hook('exchange_rate_button', grid)
692
693         self.send_button = EnterButton(_("Send"), self.do_send)
694         grid.addWidget(self.send_button, 6, 1)
695
696         b = EnterButton(_("Clear"), self.do_clear)
697         grid.addWidget(b, 6, 2)
698
699         self.payto_sig = QLabel('')
700         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
701
702         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
703         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
704         w.setLayout(grid)
705
706         def entry_changed( is_fee ):
707             self.funds_error = False
708
709             if self.amount_e.is_shortcut:
710                 self.amount_e.is_shortcut = False
711                 sendable = self.get_sendable_balance()
712                 # there is only one output because we are completely spending inputs
713                 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, self.get_payment_sources())
714                 fee = self.wallet.estimated_fee(inputs, 1)
715                 amount = total - fee
716                 self.amount_e.setText( self.format_amount(amount) )
717                 self.fee_e.setText( self.format_amount( fee ) )
718                 return
719
720             amount = self.amount_e.get_amount()
721             fee = self.fee_e.get_amount()
722
723             if not is_fee: fee = None
724             if amount is None:
725                 return
726             # assume that there will be 2 outputs (one for change)
727             inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, self.get_payment_sources())
728             if not is_fee:
729                 self.fee_e.setText( self.format_amount( fee ) )
730             if inputs:
731                 palette = QPalette()
732                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
733                 text = ""
734             else:
735                 palette = QPalette()
736                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
737                 self.funds_error = True
738                 text = _( "Not enough funds" )
739                 c, u = self.wallet.get_frozen_balance()
740                 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
741
742             self.statusBar().showMessage(text)
743             self.amount_e.setPalette(palette)
744             self.fee_e.setPalette(palette)
745
746         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
747         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
748
749         run_hook('create_send_tab', grid)
750         return w
751
752
753     def set_pay_from(self, l):
754         self.pay_from = l
755         self.from_list.clear()
756         self.from_label.setHidden(len(self.pay_from) == 0)
757         self.from_list.setHidden(len(self.pay_from) == 0)
758         for addr in self.pay_from:
759             c, u = self.wallet.get_addr_balance(addr)
760             balance = self.format_amount(c + u)
761             self.from_list.addTopLevelItem(QTreeWidgetItem( [addr, balance] ))
762
763
764     def update_completions(self):
765         l = []
766         for addr,label in self.wallet.labels.items():
767             if addr in self.wallet.addressbook:
768                 l.append( label + '  <' + addr + '>')
769
770         run_hook('update_completions', l)
771         self.completions.setStringList(l)
772
773
774     def protected(func):
775         return lambda s, *args: s.do_protect(func, args)
776
777
778     def do_send(self):
779         label = unicode( self.message_e.text() )
780
781         if self.gui_object.payment_request:
782             outputs = self.gui_object.payment_request.outputs
783             amount = self.gui_object.payment_request.get_amount()
784
785         else:
786             r = unicode( self.payto_e.text() )
787             r = r.strip()
788
789             # label or alias, with address in brackets
790             m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
791             to_address = m.group(2) if m else r
792             if not is_valid(to_address):
793                 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
794                 return
795             
796             try:
797                 amount = self.amount_e.get_amount()
798             except Exception:
799                 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
800                 return
801
802             outputs = [(to_address, amount)]
803
804         try:
805             fee = self.fee_e.get_amount()
806         except Exception:
807             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
808             return
809
810         confirm_amount = self.config.get('confirm_amount', 100000000)
811         if amount >= confirm_amount:
812             if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
813                 return
814             
815         confirm_fee = self.config.get('confirm_fee', 100000)
816         if fee >= confirm_fee:
817             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()}):
818                 return
819
820         self.send_tx(outputs, fee, label)
821
822
823
824     @protected
825     def send_tx(self, outputs, fee, label, password):
826         self.send_button.setDisabled(True)
827
828         # first, create an unsigned tx 
829         domain = self.get_payment_sources()
830         try:
831             tx = self.wallet.make_unsigned_transaction(outputs, fee, None, domain)
832             tx.error = None
833         except Exception as e:
834             traceback.print_exc(file=sys.stdout)
835             self.show_message(str(e))
836             self.send_button.setDisabled(False)
837             return
838
839         # call hook to see if plugin needs gui interaction
840         run_hook('send_tx', tx)
841
842         # sign the tx
843         def sign_thread():
844             time.sleep(0.1)
845             keypairs = {}
846             self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
847             self.wallet.sign_transaction(tx, keypairs, password)
848             return tx, fee, label
849
850         def sign_done(tx, fee, label):
851             if tx.error:
852                 self.show_message(tx.error)
853                 self.send_button.setDisabled(False)
854                 return
855             if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
856                 QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
857                 self.send_button.setDisabled(False)
858                 return
859             if label:
860                 self.wallet.set_label(tx.hash(), label)
861
862             if not self.gui_object.payment_request:
863                 if not tx.is_complete() or self.config.get('show_before_broadcast'):
864                     self.show_transaction(tx)
865                     self.do_clear()
866                     self.send_button.setDisabled(False)
867                     return
868
869             self.broadcast_transaction(tx)
870
871         self.waiting_dialog = WaitingDialog(self, 'Signing..', sign_thread, sign_done)
872         self.waiting_dialog.start()
873
874
875
876     def broadcast_transaction(self, tx):
877
878         def broadcast_thread():
879             if self.gui_object.payment_request:
880                 refund_address = self.wallet.addresses()[0]
881                 status, msg = self.gui_object.payment_request.send_ack(str(tx), refund_address)
882                 self.gui_object.payment_request = None
883             else:
884                 status, msg =  self.wallet.sendtx(tx)
885             return status, msg
886
887         def broadcast_done(status, msg):
888             if status:
889                 QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
890                 self.do_clear()
891             else:
892                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
893             self.send_button.setDisabled(False)
894
895         self.waiting_dialog = WaitingDialog(self, 'Broadcasting..', broadcast_thread, broadcast_done)
896         self.waiting_dialog.start()
897
898
899
900     def prepare_for_payment_request(self):
901         style = "QWidget { background-color:none;border:none;}"
902         self.tabs.setCurrentIndex(1)
903         for e in [self.payto_e, self.amount_e, self.message_e]:
904             e.setReadOnly(True)
905             e.setStyleSheet(style)
906         for h in [self.payto_help, self.amount_help, self.message_help]:
907             h.hide()
908         self.payto_e.setText(_("please wait..."))
909         return True
910
911     def payment_request_ok(self):
912         self.payto_e.setText(self.gui_object.payment_request.domain)
913         self.amount_e.setText(self.format_amount(self.gui_object.payment_request.get_amount()))
914         self.message_e.setText(self.gui_object.payment_request.memo)
915
916     def payment_request_error(self):
917         self.do_clear()
918         self.show_message(self.gui_object.payment_request.error)
919
920
921     def set_send(self, address, amount, label, message):
922
923         if label and self.wallet.labels.get(address) != label:
924             if self.question('Give label "%s" to address %s ?'%(label,address)):
925                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
926                     self.wallet.addressbook.append(address)
927                 self.wallet.set_label(address, label)
928
929         self.tabs.setCurrentIndex(1)
930         label = self.wallet.labels.get(address)
931         m_addr = label + '  <'+ address +'>' if label else address
932         self.payto_e.setText(m_addr)
933
934         self.message_e.setText(message)
935         if amount:
936             self.amount_e.setText(amount)
937
938
939     def do_clear(self):
940         self.payto_sig.setVisible(False)
941         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
942             e.setText('')
943             self.set_frozen(e,False)
944             e.setStyleSheet("")
945         for h in [self.payto_help, self.amount_help, self.message_help]:
946             h.show()
947
948         self.set_pay_from([])
949         self.update_status()
950
951     def set_frozen(self,entry,frozen):
952         if frozen:
953             entry.setReadOnly(True)
954             entry.setFrame(False)
955             palette = QPalette()
956             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
957             entry.setPalette(palette)
958         else:
959             entry.setReadOnly(False)
960             entry.setFrame(True)
961             palette = QPalette()
962             palette.setColor(entry.backgroundRole(), QColor('white'))
963             entry.setPalette(palette)
964
965
966     def set_addrs_frozen(self,addrs,freeze):
967         for addr in addrs:
968             if not addr: continue
969             if addr in self.wallet.frozen_addresses and not freeze:
970                 self.wallet.unfreeze(addr)
971             elif addr not in self.wallet.frozen_addresses and freeze:
972                 self.wallet.freeze(addr)
973         self.update_receive_tab()
974
975
976
977     def create_list_tab(self, headers):
978         "generic tab creation method"
979         l = MyTreeWidget(self)
980         l.setColumnCount( len(headers) )
981         l.setHeaderLabels( headers )
982
983         w = QWidget()
984         vbox = QVBoxLayout()
985         w.setLayout(vbox)
986
987         vbox.setMargin(0)
988         vbox.setSpacing(0)
989         vbox.addWidget(l)
990         buttons = QWidget()
991         vbox.addWidget(buttons)
992
993         hbox = QHBoxLayout()
994         hbox.setMargin(0)
995         hbox.setSpacing(0)
996         buttons.setLayout(hbox)
997
998         return l,w,hbox
999
1000
1001     def create_receive_tab(self):
1002         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1003         l.setContextMenuPolicy(Qt.CustomContextMenu)
1004         l.customContextMenuRequested.connect(self.create_receive_menu)
1005         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1006         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1007         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1008         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1009         self.receive_list = l
1010         self.receive_buttons_hbox = hbox
1011         hbox.addStretch(1)
1012         return w
1013
1014
1015
1016
1017     def save_column_widths(self):
1018         self.column_widths["receive"] = []
1019         for i in range(self.receive_list.columnCount() -1):
1020             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1021
1022         self.column_widths["history"] = []
1023         for i in range(self.history_list.columnCount() - 1):
1024             self.column_widths["history"].append(self.history_list.columnWidth(i))
1025
1026         self.column_widths["contacts"] = []
1027         for i in range(self.contacts_list.columnCount() - 1):
1028             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1029
1030         self.config.set_key("column_widths_2", self.column_widths, True)
1031
1032
1033     def create_contacts_tab(self):
1034         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1035         l.setContextMenuPolicy(Qt.CustomContextMenu)
1036         l.customContextMenuRequested.connect(self.create_contact_menu)
1037         for i,width in enumerate(self.column_widths['contacts']):
1038             l.setColumnWidth(i, width)
1039
1040         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1041         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1042         self.contacts_list = l
1043         self.contacts_buttons_hbox = hbox
1044         hbox.addStretch(1)
1045         return w
1046
1047
1048     def delete_imported_key(self, addr):
1049         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1050             self.wallet.delete_imported_key(addr)
1051             self.update_receive_tab()
1052             self.update_history_tab()
1053
1054     def edit_account_label(self, k):
1055         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1056         if ok:
1057             label = unicode(text)
1058             self.wallet.set_label(k,label)
1059             self.update_receive_tab()
1060
1061     def account_set_expanded(self, item, k, b):
1062         item.setExpanded(b)
1063         self.accounts_expanded[k] = b
1064
1065     def create_account_menu(self, position, k, item):
1066         menu = QMenu()
1067         if item.isExpanded():
1068             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1069         else:
1070             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1071         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1072         if self.wallet.seed_version > 4:
1073             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1074         if self.wallet.account_is_pending(k):
1075             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1076         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1077
1078     def delete_pending_account(self, k):
1079         self.wallet.delete_pending_account(k)
1080         self.update_receive_tab()
1081
1082     def create_receive_menu(self, position):
1083         # fixme: this function apparently has a side effect.
1084         # if it is not called the menu pops up several times
1085         #self.receive_list.selectedIndexes()
1086
1087         selected = self.receive_list.selectedItems()
1088         multi_select = len(selected) > 1
1089         addrs = [unicode(item.text(0)) for item in selected]
1090         if not multi_select:
1091             item = self.receive_list.itemAt(position)
1092             if not item: return
1093
1094             addr = addrs[0]
1095             if not is_valid(addr):
1096                 k = str(item.data(0,32).toString())
1097                 if k:
1098                     self.create_account_menu(position, k, item)
1099                 else:
1100                     item.setExpanded(not item.isExpanded())
1101                 return
1102
1103         menu = QMenu()
1104         if not multi_select:
1105             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1106             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1107             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1108             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1109             if not self.wallet.is_watching_only():
1110                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1111                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1112                 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1113             if self.wallet.is_imported(addr):
1114                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1115
1116         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1117             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1118         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1119             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1120
1121         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1122             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1123
1124         run_hook('receive_menu', menu, addrs)
1125         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1126
1127
1128     def get_sendable_balance(self):
1129         return sum(sum(self.wallet.get_addr_balance(a)) for a in self.get_payment_sources())
1130
1131
1132     def get_payment_sources(self):
1133         if self.pay_from:
1134             return self.pay_from
1135         else:
1136             return self.wallet.get_account_addresses(self.current_account)
1137
1138
1139     def send_from_addresses(self, addrs):
1140         self.set_pay_from( addrs )
1141         self.tabs.setCurrentIndex(1)
1142
1143
1144     def payto(self, addr):
1145         if not addr: return
1146         label = self.wallet.labels.get(addr)
1147         m_addr = label + '  <' + addr + '>' if label else addr
1148         self.tabs.setCurrentIndex(1)
1149         self.payto_e.setText(m_addr)
1150         self.amount_e.setFocus()
1151
1152
1153     def delete_contact(self, x):
1154         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1155             self.wallet.delete_contact(x)
1156             self.wallet.set_label(x, None)
1157             self.update_history_tab()
1158             self.update_contacts_tab()
1159             self.update_completions()
1160
1161
1162     def create_contact_menu(self, position):
1163         item = self.contacts_list.itemAt(position)
1164         menu = QMenu()
1165         if not item:
1166             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1167         else:
1168             addr = unicode(item.text(0))
1169             label = unicode(item.text(1))
1170             is_editable = item.data(0,32).toBool()
1171             payto_addr = item.data(0,33).toString()
1172             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1173             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1174             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1175             if is_editable:
1176                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1177                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1178
1179         run_hook('create_contact_menu', menu, item)
1180         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1181
1182
1183     def update_receive_item(self, item):
1184         item.setFont(0, QFont(MONOSPACE_FONT))
1185         address = str(item.data(0,0).toString())
1186         label = self.wallet.labels.get(address,'')
1187         item.setData(1,0,label)
1188         item.setData(0,32, True) # is editable
1189
1190         run_hook('update_receive_item', address, item)
1191
1192         if not self.wallet.is_mine(address): return
1193
1194         c, u = self.wallet.get_addr_balance(address)
1195         balance = self.format_amount(c + u)
1196         item.setData(2,0,balance)
1197
1198         if address in self.wallet.frozen_addresses:
1199             item.setBackgroundColor(0, QColor('lightblue'))
1200
1201
1202     def update_receive_tab(self):
1203         l = self.receive_list
1204         # extend the syntax for consistency
1205         l.addChild = l.addTopLevelItem
1206         l.insertChild = l.insertTopLevelItem
1207
1208         l.clear()
1209         for i,width in enumerate(self.column_widths['receive']):
1210             l.setColumnWidth(i, width)
1211
1212         accounts = self.wallet.get_accounts()
1213         if self.current_account is None:
1214             account_items = sorted(accounts.items())
1215         else:
1216             account_items = [(self.current_account, accounts.get(self.current_account))]
1217
1218
1219         for k, account in account_items:
1220
1221             if len(accounts) > 1:
1222                 name = self.wallet.get_account_name(k)
1223                 c,u = self.wallet.get_account_balance(k)
1224                 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1225                 l.addTopLevelItem(account_item)
1226                 account_item.setExpanded(self.accounts_expanded.get(k, True))
1227                 account_item.setData(0, 32, k)
1228             else:
1229                 account_item = l
1230
1231             sequences = [0,1] if account.has_change() else [0]
1232             for is_change in sequences:
1233                 if len(sequences) > 1:
1234                     name = _("Receiving") if not is_change else _("Change")
1235                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1236                     account_item.addChild(seq_item)
1237                     if not is_change: 
1238                         seq_item.setExpanded(True)
1239                 else:
1240                     seq_item = account_item
1241                     
1242                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1243                 used_flag = False
1244
1245                 is_red = False
1246                 gap = 0
1247
1248                 for address in account.get_addresses(is_change):
1249
1250                     num, is_used = self.wallet.is_used(address)
1251                     if num == 0:
1252                         gap += 1
1253                         if gap > self.wallet.gap_limit:
1254                             is_red = True
1255                     else:
1256                         gap = 0
1257
1258                     item = QTreeWidgetItem( [ address, '', '', "%d"%num] )
1259                     self.update_receive_item(item)
1260                     if is_red:
1261                         item.setBackgroundColor(1, QColor('red'))
1262
1263                     if is_used:
1264                         if not used_flag:
1265                             seq_item.insertChild(0,used_item)
1266                             used_flag = True
1267                         used_item.addChild(item)
1268                     else:
1269                         seq_item.addChild(item)
1270
1271         # we use column 1 because column 0 may be hidden
1272         l.setCurrentItem(l.topLevelItem(0),1)
1273
1274
1275     def update_contacts_tab(self):
1276         l = self.contacts_list
1277         l.clear()
1278
1279         for address in self.wallet.addressbook:
1280             label = self.wallet.labels.get(address,'')
1281             n = self.wallet.get_num_tx(address)
1282             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1283             item.setFont(0, QFont(MONOSPACE_FONT))
1284             # 32 = label can be edited (bool)
1285             item.setData(0,32, True)
1286             # 33 = payto string
1287             item.setData(0,33, address)
1288             l.addTopLevelItem(item)
1289
1290         run_hook('update_contacts_tab', l)
1291         l.setCurrentItem(l.topLevelItem(0))
1292
1293
1294
1295     def create_console_tab(self):
1296         from console import Console
1297         self.console = console = Console()
1298         return console
1299
1300
1301     def update_console(self):
1302         console = self.console
1303         console.history = self.config.get("console-history",[])
1304         console.history_index = len(console.history)
1305
1306         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1307         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1308
1309         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1310         methods = {}
1311         def mkfunc(f, method):
1312             return lambda *args: apply( f, (method, args, self.password_dialog ))
1313         for m in dir(c):
1314             if m[0]=='_' or m in ['network','wallet']: continue
1315             methods[m] = mkfunc(c._run, m)
1316
1317         console.updateNamespace(methods)
1318
1319
1320     def change_account(self,s):
1321         if s == _("All accounts"):
1322             self.current_account = None
1323         else:
1324             accounts = self.wallet.get_account_names()
1325             for k, v in accounts.items():
1326                 if v == s:
1327                     self.current_account = k
1328         self.update_history_tab()
1329         self.update_status()
1330         self.update_receive_tab()
1331
1332     def create_status_bar(self):
1333
1334         sb = QStatusBar()
1335         sb.setFixedHeight(35)
1336         qtVersion = qVersion()
1337
1338         self.balance_label = QLabel("")
1339         sb.addWidget(self.balance_label)
1340
1341         from version_getter import UpdateLabel
1342         self.updatelabel = UpdateLabel(self.config, sb)
1343
1344         self.account_selector = QComboBox()
1345         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1346         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1347         sb.addPermanentWidget(self.account_selector)
1348
1349         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1350             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1351
1352         self.lock_icon = QIcon()
1353         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1354         sb.addPermanentWidget( self.password_button )
1355
1356         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1357         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1358         sb.addPermanentWidget( self.seed_button )
1359         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1360         sb.addPermanentWidget( self.status_button )
1361
1362         run_hook('create_status_bar', (sb,))
1363
1364         self.setStatusBar(sb)
1365
1366
1367     def update_lock_icon(self):
1368         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1369         self.password_button.setIcon( icon )
1370
1371
1372     def update_buttons_on_seed(self):
1373         if self.wallet.has_seed():
1374            self.seed_button.show()
1375         else:
1376            self.seed_button.hide()
1377
1378         if not self.wallet.is_watching_only():
1379            self.password_button.show()
1380            self.send_button.setText(_("Send"))
1381         else:
1382            self.password_button.hide()
1383            self.send_button.setText(_("Create unsigned transaction"))
1384
1385
1386     def change_password_dialog(self):
1387         from password_dialog import PasswordDialog
1388         d = PasswordDialog(self.wallet, self)
1389         d.run()
1390         self.update_lock_icon()
1391
1392
1393     def new_contact_dialog(self):
1394
1395         d = QDialog(self)
1396         d.setWindowTitle(_("New Contact"))
1397         vbox = QVBoxLayout(d)
1398         vbox.addWidget(QLabel(_('New Contact')+':'))
1399
1400         grid = QGridLayout()
1401         line1 = QLineEdit()
1402         line2 = QLineEdit()
1403         grid.addWidget(QLabel(_("Address")), 1, 0)
1404         grid.addWidget(line1, 1, 1)
1405         grid.addWidget(QLabel(_("Name")), 2, 0)
1406         grid.addWidget(line2, 2, 1)
1407
1408         vbox.addLayout(grid)
1409         vbox.addLayout(ok_cancel_buttons(d))
1410
1411         if not d.exec_():
1412             return
1413
1414         address = str(line1.text())
1415         label = unicode(line2.text())
1416
1417         if not is_valid(address):
1418             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1419             return
1420
1421         self.wallet.add_contact(address)
1422         if label:
1423             self.wallet.set_label(address, label)
1424
1425         self.update_contacts_tab()
1426         self.update_history_tab()
1427         self.update_completions()
1428         self.tabs.setCurrentIndex(3)
1429
1430
1431     @protected
1432     def new_account_dialog(self, password):
1433
1434         dialog = QDialog(self)
1435         dialog.setModal(1)
1436         dialog.setWindowTitle(_("New Account"))
1437
1438         vbox = QVBoxLayout()
1439         vbox.addWidget(QLabel(_('Account name')+':'))
1440         e = QLineEdit()
1441         vbox.addWidget(e)
1442         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1443             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1444         l = QLabel(msg)
1445         l.setWordWrap(True)
1446         vbox.addWidget(l)
1447
1448         vbox.addLayout(ok_cancel_buttons(dialog))
1449         dialog.setLayout(vbox)
1450         r = dialog.exec_()
1451         if not r: return
1452
1453         name = str(e.text())
1454         if not name: return
1455
1456         self.wallet.create_pending_account(name, password)
1457         self.update_receive_tab()
1458         self.tabs.setCurrentIndex(2)
1459
1460
1461
1462
1463     def show_master_public_keys(self):
1464
1465         dialog = QDialog(self)
1466         dialog.setModal(1)
1467         dialog.setWindowTitle(_("Master Public Keys"))
1468
1469         main_layout = QGridLayout()
1470         mpk_dict = self.wallet.get_master_public_keys()
1471         i = 0
1472         for key, value in mpk_dict.items():
1473             main_layout.addWidget(QLabel(key), i, 0)
1474             mpk_text = QTextEdit()
1475             mpk_text.setReadOnly(True)
1476             mpk_text.setMaximumHeight(170)
1477             mpk_text.setText(value)
1478             main_layout.addWidget(mpk_text, i + 1, 0)
1479             i += 2
1480
1481         vbox = QVBoxLayout()
1482         vbox.addLayout(main_layout)
1483         vbox.addLayout(close_button(dialog))
1484
1485         dialog.setLayout(vbox)
1486         dialog.exec_()
1487
1488
1489     @protected
1490     def show_seed_dialog(self, password):
1491         if not self.wallet.has_seed():
1492             QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1493             return
1494
1495         try:
1496             mnemonic = self.wallet.get_mnemonic(password)
1497         except Exception:
1498             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1499             return
1500         from seed_dialog import SeedDialog
1501         d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1502         d.exec_()
1503
1504
1505
1506     def show_qrcode(self, data, title = _("QR code")):
1507         if not data: return
1508         d = QDialog(self)
1509         d.setModal(1)
1510         d.setWindowTitle(title)
1511         d.setMinimumSize(270, 300)
1512         vbox = QVBoxLayout()
1513         qrw = QRCodeWidget(data)
1514         vbox.addWidget(qrw, 1)
1515         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1516         hbox = QHBoxLayout()
1517         hbox.addStretch(1)
1518
1519         filename = os.path.join(self.config.path, "qrcode.bmp")
1520
1521         def print_qr():
1522             bmp.save_qrcode(qrw.qr, filename)
1523             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1524
1525         def copy_to_clipboard():
1526             bmp.save_qrcode(qrw.qr, filename)
1527             self.app.clipboard().setImage(QImage(filename))
1528             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1529
1530         b = QPushButton(_("Copy"))
1531         hbox.addWidget(b)
1532         b.clicked.connect(copy_to_clipboard)
1533
1534         b = QPushButton(_("Save"))
1535         hbox.addWidget(b)
1536         b.clicked.connect(print_qr)
1537
1538         b = QPushButton(_("Close"))
1539         hbox.addWidget(b)
1540         b.clicked.connect(d.accept)
1541         b.setDefault(True)
1542
1543         vbox.addLayout(hbox)
1544         d.setLayout(vbox)
1545         d.exec_()
1546
1547
1548     def do_protect(self, func, args):
1549         if self.wallet.use_encryption:
1550             password = self.password_dialog()
1551             if not password:
1552                 return
1553         else:
1554             password = None
1555
1556         if args != (False,):
1557             args = (self,) + args + (password,)
1558         else:
1559             args = (self,password)
1560         apply( func, args)
1561
1562
1563     def show_public_keys(self, address):
1564         if not address: return
1565         try:
1566             pubkey_list = self.wallet.get_public_keys(address)
1567         except Exception as e:
1568             traceback.print_exc(file=sys.stdout)
1569             self.show_message(str(e))
1570             return
1571
1572         d = QDialog(self)
1573         d.setMinimumSize(600, 200)
1574         d.setModal(1)
1575         vbox = QVBoxLayout()
1576         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1577         vbox.addWidget( QLabel(_("Public key") + ':'))
1578         keys = QTextEdit()
1579         keys.setReadOnly(True)
1580         keys.setText('\n'.join(pubkey_list))
1581         vbox.addWidget(keys)
1582         #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1583         vbox.addLayout(close_button(d))
1584         d.setLayout(vbox)
1585         d.exec_()
1586
1587     @protected
1588     def show_private_key(self, address, password):
1589         if not address: return
1590         try:
1591             pk_list = self.wallet.get_private_key(address, password)
1592         except Exception as e:
1593             traceback.print_exc(file=sys.stdout)
1594             self.show_message(str(e))
1595             return
1596
1597         d = QDialog(self)
1598         d.setMinimumSize(600, 200)
1599         d.setModal(1)
1600         vbox = QVBoxLayout()
1601         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1602         vbox.addWidget( QLabel(_("Private key") + ':'))
1603         keys = QTextEdit()
1604         keys.setReadOnly(True)
1605         keys.setText('\n'.join(pk_list))
1606         vbox.addWidget(keys)
1607         vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1608         vbox.addLayout(close_button(d))
1609         d.setLayout(vbox)
1610         d.exec_()
1611
1612
1613     @protected
1614     def do_sign(self, address, message, signature, password):
1615         message = unicode(message.toPlainText())
1616         message = message.encode('utf-8')
1617         try:
1618             sig = self.wallet.sign_message(str(address.text()), message, password)
1619             signature.setText(sig)
1620         except Exception as e:
1621             self.show_message(str(e))
1622
1623     def do_verify(self, address, message, signature):
1624         message = unicode(message.toPlainText())
1625         message = message.encode('utf-8')
1626         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1627             self.show_message(_("Signature verified"))
1628         else:
1629             self.show_message(_("Error: wrong signature"))
1630
1631
1632     def sign_verify_message(self, address=''):
1633         d = QDialog(self)
1634         d.setModal(1)
1635         d.setWindowTitle(_('Sign/verify Message'))
1636         d.setMinimumSize(410, 290)
1637
1638         layout = QGridLayout(d)
1639
1640         message_e = QTextEdit()
1641         layout.addWidget(QLabel(_('Message')), 1, 0)
1642         layout.addWidget(message_e, 1, 1)
1643         layout.setRowStretch(2,3)
1644
1645         address_e = QLineEdit()
1646         address_e.setText(address)
1647         layout.addWidget(QLabel(_('Address')), 2, 0)
1648         layout.addWidget(address_e, 2, 1)
1649
1650         signature_e = QTextEdit()
1651         layout.addWidget(QLabel(_('Signature')), 3, 0)
1652         layout.addWidget(signature_e, 3, 1)
1653         layout.setRowStretch(3,1)
1654
1655         hbox = QHBoxLayout()
1656
1657         b = QPushButton(_("Sign"))
1658         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1659         hbox.addWidget(b)
1660
1661         b = QPushButton(_("Verify"))
1662         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1663         hbox.addWidget(b)
1664
1665         b = QPushButton(_("Close"))
1666         b.clicked.connect(d.accept)
1667         hbox.addWidget(b)
1668         layout.addLayout(hbox, 4, 1)
1669         d.exec_()
1670
1671
1672     @protected
1673     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1674         try:
1675             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1676             message_e.setText(decrypted)
1677         except Exception as e:
1678             self.show_message(str(e))
1679
1680
1681     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1682         message = unicode(message_e.toPlainText())
1683         message = message.encode('utf-8')
1684         try:
1685             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1686             encrypted_e.setText(encrypted)
1687         except Exception as e:
1688             self.show_message(str(e))
1689
1690
1691
1692     def encrypt_message(self, address = ''):
1693         d = QDialog(self)
1694         d.setModal(1)
1695         d.setWindowTitle(_('Encrypt/decrypt Message'))
1696         d.setMinimumSize(610, 490)
1697
1698         layout = QGridLayout(d)
1699
1700         message_e = QTextEdit()
1701         layout.addWidget(QLabel(_('Message')), 1, 0)
1702         layout.addWidget(message_e, 1, 1)
1703         layout.setRowStretch(2,3)
1704
1705         pubkey_e = QLineEdit()
1706         if address:
1707             pubkey = self.wallet.getpubkeys(address)[0]
1708             pubkey_e.setText(pubkey)
1709         layout.addWidget(QLabel(_('Public key')), 2, 0)
1710         layout.addWidget(pubkey_e, 2, 1)
1711
1712         encrypted_e = QTextEdit()
1713         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1714         layout.addWidget(encrypted_e, 3, 1)
1715         layout.setRowStretch(3,1)
1716
1717         hbox = QHBoxLayout()
1718         b = QPushButton(_("Encrypt"))
1719         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1720         hbox.addWidget(b)
1721
1722         b = QPushButton(_("Decrypt"))
1723         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1724         hbox.addWidget(b)
1725
1726         b = QPushButton(_("Close"))
1727         b.clicked.connect(d.accept)
1728         hbox.addWidget(b)
1729
1730         layout.addLayout(hbox, 4, 1)
1731         d.exec_()
1732
1733
1734     def question(self, msg):
1735         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1736
1737     def show_message(self, msg):
1738         QMessageBox.information(self, _('Message'), msg, _('OK'))
1739
1740     def password_dialog(self, msg=None):
1741         d = QDialog(self)
1742         d.setModal(1)
1743         d.setWindowTitle(_("Enter Password"))
1744
1745         pw = QLineEdit()
1746         pw.setEchoMode(2)
1747
1748         vbox = QVBoxLayout()
1749         if not msg:
1750             msg = _('Please enter your password')
1751         vbox.addWidget(QLabel(msg))
1752
1753         grid = QGridLayout()
1754         grid.setSpacing(8)
1755         grid.addWidget(QLabel(_('Password')), 1, 0)
1756         grid.addWidget(pw, 1, 1)
1757         vbox.addLayout(grid)
1758
1759         vbox.addLayout(ok_cancel_buttons(d))
1760         d.setLayout(vbox)
1761
1762         run_hook('password_dialog', pw, grid, 1)
1763         if not d.exec_(): return
1764         return unicode(pw.text())
1765
1766
1767
1768
1769
1770
1771
1772
1773     def tx_from_text(self, txt):
1774         "json or raw hexadecimal"
1775         try:
1776             txt.decode('hex')
1777             tx = Transaction(txt)
1778             return tx
1779         except Exception:
1780             pass
1781
1782         try:
1783             tx_dict = json.loads(str(txt))
1784             assert "hex" in tx_dict.keys()
1785             tx = Transaction(tx_dict["hex"])
1786             if tx_dict.has_key("input_info"):
1787                 input_info = json.loads(tx_dict['input_info'])
1788                 tx.add_input_info(input_info)
1789             return tx
1790         except Exception:
1791             traceback.print_exc(file=sys.stdout)
1792             pass
1793
1794         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1795
1796
1797
1798     def read_tx_from_file(self):
1799         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1800         if not fileName:
1801             return
1802         try:
1803             with open(fileName, "r") as f:
1804                 file_content = f.read()
1805         except (ValueError, IOError, os.error), reason:
1806             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1807
1808         return self.tx_from_text(file_content)
1809
1810
1811     @protected
1812     def sign_raw_transaction(self, tx, input_info, password):
1813         self.wallet.signrawtransaction(tx, input_info, [], password)
1814
1815     def do_process_from_text(self):
1816         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1817         if not text:
1818             return
1819         tx = self.tx_from_text(text)
1820         if tx:
1821             self.show_transaction(tx)
1822
1823     def do_process_from_file(self):
1824         tx = self.read_tx_from_file()
1825         if tx:
1826             self.show_transaction(tx)
1827
1828     def do_process_from_txid(self):
1829         from electrum import transaction
1830         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1831         if ok and txid:
1832             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1833             if r:
1834                 tx = transaction.Transaction(r)
1835                 if tx:
1836                     self.show_transaction(tx)
1837                 else:
1838                     self.show_message("unknown transaction")
1839
1840     def do_process_from_csvReader(self, csvReader):
1841         outputs = []
1842         errors = []
1843         errtext = ""
1844         try:
1845             for position, row in enumerate(csvReader):
1846                 address = row[0]
1847                 if not is_valid(address):
1848                     errors.append((position, address))
1849                     continue
1850                 amount = Decimal(row[1])
1851                 amount = int(100000000*amount)
1852                 outputs.append((address, amount))
1853         except (ValueError, IOError, os.error), reason:
1854             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1855             return
1856         if errors != []:
1857             for x in errors:
1858                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1859             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1860             return
1861
1862         try:
1863             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1864         except Exception as e:
1865             self.show_message(str(e))
1866             return
1867
1868         self.show_transaction(tx)
1869
1870     def do_process_from_csv_file(self):
1871         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1872         if not fileName:
1873             return
1874         try:
1875             with open(fileName, "r") as f:
1876                 csvReader = csv.reader(f)
1877                 self.do_process_from_csvReader(csvReader)
1878         except (ValueError, IOError, os.error), reason:
1879             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1880             return
1881
1882     def do_process_from_csv_text(self):
1883         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
1884                                + _("Format: address, amount. One output per line"), _("Load CSV"))
1885         if not text:
1886             return
1887         f = StringIO.StringIO(text)
1888         csvReader = csv.reader(f)
1889         self.do_process_from_csvReader(csvReader)
1890
1891
1892
1893     @protected
1894     def export_privkeys_dialog(self, password):
1895         if self.wallet.is_watching_only():
1896             self.show_message(_("This is a watching-only wallet"))
1897             return
1898
1899         d = QDialog(self)
1900         d.setWindowTitle(_('Private keys'))
1901         d.setMinimumSize(850, 300)
1902         vbox = QVBoxLayout(d)
1903
1904         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), 
1905                               _("Exposing a single private key can compromise your entire wallet!"), 
1906                               _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
1907         vbox.addWidget(QLabel(msg))
1908
1909         e = QTextEdit()
1910         e.setReadOnly(True)
1911         vbox.addWidget(e)
1912
1913         defaultname = 'electrum-private-keys.csv'
1914         select_msg = _('Select file to export your private keys to')
1915         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
1916         vbox.addLayout(hbox)
1917
1918         h, b = ok_cancel_buttons2(d, _('Export'))
1919         b.setEnabled(False)
1920         vbox.addLayout(h)
1921
1922         private_keys = {}
1923         addresses = self.wallet.addresses(True)
1924         done = False
1925         def privkeys_thread():
1926             for addr in addresses:
1927                 time.sleep(0.1)
1928                 if done: 
1929                     break
1930                 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
1931                 d.emit(SIGNAL('computing_privkeys'))
1932             d.emit(SIGNAL('show_privkeys'))
1933
1934         def show_privkeys():
1935             s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
1936             e.setText(s)
1937             b.setEnabled(True)
1938
1939         d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
1940         d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
1941         threading.Thread(target=privkeys_thread).start()
1942
1943         if not d.exec_():
1944             done = True
1945             return
1946
1947         filename = filename_e.text()
1948         if not filename:
1949             return
1950
1951         try:
1952             self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
1953         except (IOError, os.error), reason:
1954             export_error_label = _("Electrum was unable to produce a private key-export.")
1955             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
1956
1957         except Exception as e:
1958             self.show_message(str(e))
1959             return
1960
1961         self.show_message(_("Private keys exported."))
1962
1963
1964     def do_export_privkeys(self, fileName, pklist, is_csv):
1965         with open(fileName, "w+") as f:
1966             if is_csv:
1967                 transaction = csv.writer(f)
1968                 transaction.writerow(["address", "private_key"])
1969                 for addr, pk in pklist.items():
1970                     transaction.writerow(["%34s"%addr,pk])
1971             else:
1972                 import json
1973                 f.write(json.dumps(pklist, indent = 4))
1974
1975
1976     def do_import_labels(self):
1977         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1978         if not labelsFile: return
1979         try:
1980             f = open(labelsFile, 'r')
1981             data = f.read()
1982             f.close()
1983             for key, value in json.loads(data).items():
1984                 self.wallet.set_label(key, value)
1985             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1986         except (IOError, os.error), reason:
1987             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1988
1989
1990     def do_export_labels(self):
1991         labels = self.wallet.labels
1992         try:
1993             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1994             if fileName:
1995                 with open(fileName, 'w+') as f:
1996                     json.dump(labels, f)
1997                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
1998         except (IOError, os.error), reason:
1999             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2000
2001
2002     def export_history_dialog(self):
2003
2004         d = QDialog(self)
2005         d.setWindowTitle(_('Export History'))
2006         d.setMinimumSize(400, 200)
2007         vbox = QVBoxLayout(d)
2008
2009         defaultname = os.path.expanduser('~/electrum-history.csv')
2010         select_msg = _('Select file to export your wallet transactions to')
2011
2012         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2013         vbox.addLayout(hbox)
2014
2015         vbox.addStretch(1)
2016
2017         h, b = ok_cancel_buttons2(d, _('Export'))
2018         vbox.addLayout(h)
2019         if not d.exec_():
2020             return
2021
2022         filename = filename_e.text()
2023         if not filename:
2024             return
2025
2026         try:
2027             self.do_export_history(self.wallet, filename, csv_button.isChecked())
2028         except (IOError, os.error), reason:
2029             export_error_label = _("Electrum was unable to produce a transaction export.")
2030             QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2031             return
2032
2033         QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2034
2035
2036     def do_export_history(self, wallet, fileName, is_csv):
2037         history = wallet.get_tx_history()
2038         lines = []
2039         for item in history:
2040             tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2041             if confirmations:
2042                 if timestamp is not None:
2043                     try:
2044                         time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2045                     except [RuntimeError, TypeError, NameError] as reason:
2046                         time_string = "unknown"
2047                         pass
2048                 else:
2049                     time_string = "unknown"
2050             else:
2051                 time_string = "pending"
2052
2053             if value is not None:
2054                 value_string = format_satoshis(value, True)
2055             else:
2056                 value_string = '--'
2057
2058             if fee is not None:
2059                 fee_string = format_satoshis(fee, True)
2060             else:
2061                 fee_string = '0'
2062
2063             if tx_hash:
2064                 label, is_default_label = wallet.get_label(tx_hash)
2065                 label = label.encode('utf-8')
2066             else:
2067                 label = ""
2068
2069             balance_string = format_satoshis(balance, False)
2070             if is_csv:
2071                 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2072             else:
2073                 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2074
2075         with open(fileName, "w+") as f:
2076             if is_csv:
2077                 transaction = csv.writer(f)
2078                 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2079                 for line in lines:
2080                     transaction.writerow(line)
2081             else:
2082                 import json
2083                 f.write(json.dumps(lines, indent = 4))
2084
2085
2086     def sweep_key_dialog(self):
2087         d = QDialog(self)
2088         d.setWindowTitle(_('Sweep private keys'))
2089         d.setMinimumSize(600, 300)
2090
2091         vbox = QVBoxLayout(d)
2092         vbox.addWidget(QLabel(_("Enter private keys")))
2093
2094         keys_e = QTextEdit()
2095         keys_e.setTabChangesFocus(True)
2096         vbox.addWidget(keys_e)
2097
2098         h, address_e = address_field(self.wallet.addresses())
2099         vbox.addLayout(h)
2100
2101         vbox.addStretch(1)
2102         hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2103         vbox.addLayout(hbox)
2104         button.setEnabled(False)
2105
2106         def get_address():
2107             addr = str(address_e.text())
2108             if bitcoin.is_address(addr):
2109                 return addr
2110
2111         def get_pk():
2112             pk = str(keys_e.toPlainText()).strip()
2113             if Wallet.is_private_key(pk):
2114                 return pk.split()
2115
2116         f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2117         keys_e.textChanged.connect(f)
2118         address_e.textChanged.connect(f)
2119         if not d.exec_():
2120             return
2121
2122         fee = self.wallet.fee
2123         tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2124         self.show_transaction(tx)
2125
2126
2127     @protected
2128     def do_import_privkey(self, password):
2129         if not self.wallet.has_imported_keys():
2130             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2131                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2132                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2133             if r == 4: return
2134
2135         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2136         if not text: return
2137
2138         text = str(text).split()
2139         badkeys = []
2140         addrlist = []
2141         for key in text:
2142             try:
2143                 addr = self.wallet.import_key(key, password)
2144             except Exception as e:
2145                 badkeys.append(key)
2146                 continue
2147             if not addr:
2148                 badkeys.append(key)
2149             else:
2150                 addrlist.append(addr)
2151         if addrlist:
2152             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2153         if badkeys:
2154             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2155         self.update_receive_tab()
2156         self.update_history_tab()
2157
2158
2159     def settings_dialog(self):
2160         d = QDialog(self)
2161         d.setWindowTitle(_('Electrum Settings'))
2162         d.setModal(1)
2163         vbox = QVBoxLayout()
2164         grid = QGridLayout()
2165         grid.setColumnStretch(0,1)
2166
2167         nz_label = QLabel(_('Display zeros') + ':')
2168         grid.addWidget(nz_label, 0, 0)
2169         nz_e = AmountEdit(None,True)
2170         nz_e.setText("%d"% self.num_zeros)
2171         grid.addWidget(nz_e, 0, 1)
2172         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2173         grid.addWidget(HelpButton(msg), 0, 2)
2174         if not self.config.is_modifiable('num_zeros'):
2175             for w in [nz_e, nz_label]: w.setEnabled(False)
2176
2177         lang_label=QLabel(_('Language') + ':')
2178         grid.addWidget(lang_label, 1, 0)
2179         lang_combo = QComboBox()
2180         from electrum.i18n import languages
2181         lang_combo.addItems(languages.values())
2182         try:
2183             index = languages.keys().index(self.config.get("language",''))
2184         except Exception:
2185             index = 0
2186         lang_combo.setCurrentIndex(index)
2187         grid.addWidget(lang_combo, 1, 1)
2188         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2189         if not self.config.is_modifiable('language'):
2190             for w in [lang_combo, lang_label]: w.setEnabled(False)
2191
2192
2193         fee_label = QLabel(_('Transaction fee') + ':')
2194         grid.addWidget(fee_label, 2, 0)
2195         fee_e = AmountEdit(self.get_decimal_point)
2196         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2197         grid.addWidget(fee_e, 2, 1)
2198         msg = _('Fee per kilobyte of transaction.') + ' ' \
2199             + _('Recommended value') + ': ' + self.format_amount(20000)
2200         grid.addWidget(HelpButton(msg), 2, 2)
2201         if not self.config.is_modifiable('fee_per_kb'):
2202             for w in [fee_e, fee_label]: w.setEnabled(False)
2203
2204         units = ['BTC', 'mBTC']
2205         unit_label = QLabel(_('Base unit') + ':')
2206         grid.addWidget(unit_label, 3, 0)
2207         unit_combo = QComboBox()
2208         unit_combo.addItems(units)
2209         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2210         grid.addWidget(unit_combo, 3, 1)
2211         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2212                                              + '\n1BTC=1000mBTC.\n' \
2213                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2214
2215         usechange_cb = QCheckBox(_('Use change addresses'))
2216         usechange_cb.setChecked(self.wallet.use_change)
2217         grid.addWidget(usechange_cb, 4, 0)
2218         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2219         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2220
2221         block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2222         block_ex_label = QLabel(_('Online Block Explorer') + ':')
2223         grid.addWidget(block_ex_label, 5, 0)
2224         block_ex_combo = QComboBox()
2225         block_ex_combo.addItems(block_explorers)
2226         block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2227         grid.addWidget(block_ex_combo, 5, 1)
2228         grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2229
2230         show_tx = self.config.get('show_before_broadcast', False)
2231         showtx_cb = QCheckBox(_('Show before broadcast'))
2232         showtx_cb.setChecked(show_tx)
2233         grid.addWidget(showtx_cb, 6, 0)
2234         grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2235
2236         vbox.addLayout(grid)
2237         vbox.addStretch(1)
2238         vbox.addLayout(ok_cancel_buttons(d))
2239         d.setLayout(vbox)
2240
2241         # run the dialog
2242         if not d.exec_(): return
2243
2244         try:
2245             fee = self.fee_e.get_amount()
2246         except Exception:
2247             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2248             return
2249
2250         self.wallet.set_fee(fee)
2251
2252         nz = unicode(nz_e.text())
2253         try:
2254             nz = int( nz )
2255             if nz>8: nz=8
2256         except Exception:
2257             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2258             return
2259
2260         if self.num_zeros != nz:
2261             self.num_zeros = nz
2262             self.config.set_key('num_zeros', nz, True)
2263             self.update_history_tab()
2264             self.update_receive_tab()
2265
2266         usechange_result = usechange_cb.isChecked()
2267         if self.wallet.use_change != usechange_result:
2268             self.wallet.use_change = usechange_result
2269             self.wallet.storage.put('use_change', self.wallet.use_change)
2270
2271         if showtx_cb.isChecked() != show_tx:
2272             self.config.set_key('show_before_broadcast', not show_tx)
2273
2274         unit_result = units[unit_combo.currentIndex()]
2275         if self.base_unit() != unit_result:
2276             self.decimal_point = 8 if unit_result == 'BTC' else 5
2277             self.config.set_key('decimal_point', self.decimal_point, True)
2278             self.update_history_tab()
2279             self.update_status()
2280
2281         need_restart = False
2282
2283         lang_request = languages.keys()[lang_combo.currentIndex()]
2284         if lang_request != self.config.get('language'):
2285             self.config.set_key("language", lang_request, True)
2286             need_restart = True
2287
2288         be_result = block_explorers[block_ex_combo.currentIndex()]
2289         self.config.set_key('block_explorer', be_result, True)
2290
2291         run_hook('close_settings_dialog')
2292
2293         if need_restart:
2294             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2295
2296
2297     def run_network_dialog(self):
2298         if not self.network:
2299             return
2300         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2301
2302     def closeEvent(self, event):
2303         self.tray.hide()
2304         self.config.set_key("is_maximized", self.isMaximized())
2305         if not self.isMaximized():
2306             g = self.geometry()
2307             self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2308         self.save_column_widths()
2309         self.config.set_key("console-history", self.console.history[-50:], True)
2310         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2311         event.accept()
2312
2313
2314     def plugins_dialog(self):
2315         from electrum.plugins import plugins
2316
2317         d = QDialog(self)
2318         d.setWindowTitle(_('Electrum Plugins'))
2319         d.setModal(1)
2320
2321         vbox = QVBoxLayout(d)
2322
2323         # plugins
2324         scroll = QScrollArea()
2325         scroll.setEnabled(True)
2326         scroll.setWidgetResizable(True)
2327         scroll.setMinimumSize(400,250)
2328         vbox.addWidget(scroll)
2329
2330         w = QWidget()
2331         scroll.setWidget(w)
2332         w.setMinimumHeight(len(plugins)*35)
2333
2334         grid = QGridLayout()
2335         grid.setColumnStretch(0,1)
2336         w.setLayout(grid)
2337
2338         def do_toggle(cb, p, w):
2339             r = p.toggle()
2340             cb.setChecked(r)
2341             if w: w.setEnabled(r)
2342
2343         def mk_toggle(cb, p, w):
2344             return lambda: do_toggle(cb,p,w)
2345
2346         for i, p in enumerate(plugins):
2347             try:
2348                 cb = QCheckBox(p.fullname())
2349                 cb.setDisabled(not p.is_available())
2350                 cb.setChecked(p.is_enabled())
2351                 grid.addWidget(cb, i, 0)
2352                 if p.requires_settings():
2353                     w = p.settings_widget(self)
2354                     w.setEnabled( p.is_enabled() )
2355                     grid.addWidget(w, i, 1)
2356                 else:
2357                     w = None
2358                 cb.clicked.connect(mk_toggle(cb,p,w))
2359                 grid.addWidget(HelpButton(p.description()), i, 2)
2360             except Exception:
2361                 print_msg(_("Error: cannot display plugin"), p)
2362                 traceback.print_exc(file=sys.stdout)
2363         grid.setRowStretch(i+1,1)
2364
2365         vbox.addLayout(close_button(d))
2366
2367         d.exec_()
2368
2369
2370     def show_account_details(self, k):
2371         account = self.wallet.accounts[k]
2372
2373         d = QDialog(self)
2374         d.setWindowTitle(_('Account Details'))
2375         d.setModal(1)
2376
2377         vbox = QVBoxLayout(d)
2378         name = self.wallet.get_account_name(k)
2379         label = QLabel('Name: ' + name)
2380         vbox.addWidget(label)
2381
2382         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2383
2384         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2385
2386         vbox.addWidget(QLabel(_('Master Public Key:')))
2387
2388         text = QTextEdit()
2389         text.setReadOnly(True)
2390         text.setMaximumHeight(170)
2391         vbox.addWidget(text)
2392
2393         mpk_text = '\n'.join( account.get_master_pubkeys() )
2394         text.setText(mpk_text)
2395
2396         vbox.addLayout(close_button(d))
2397         d.exec_()