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