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