always enable qr scanner plugin
[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'),_('Amount'), _('Status')])
1320         l.setColumnWidth(0, 150)
1321         h = l.header()
1322         h.setStretchLastSection(False)
1323         h.setResizeMode(1, QHeaderView.Stretch)
1324         l.setContextMenuPolicy(Qt.CustomContextMenu)
1325         l.customContextMenuRequested.connect(self.create_invoice_menu)
1326         self.invoices_list = l
1327         return w
1328
1329     def update_invoices_tab(self):
1330         invoices = self.wallet.storage.get('invoices', {})
1331         l = self.invoices_list
1332         l.clear()
1333         for key, value in invoices.items():
1334             try:
1335                 domain, memo, amount, expiration_date, status, tx_hash = value
1336             except:
1337                 invoices.pop(key)
1338                 continue
1339             if status == PR_UNPAID and expiration_date and expiration_date < time.time():
1340                 status = PR_EXPIRED
1341             item = QTreeWidgetItem( [ domain, memo, self.format_amount(amount), format_status(status)] )
1342             l.addTopLevelItem(item)
1343
1344         l.setCurrentItem(l.topLevelItem(0))
1345
1346
1347
1348     def delete_imported_key(self, addr):
1349         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1350             self.wallet.delete_imported_key(addr)
1351             self.update_address_tab()
1352             self.update_history_tab()
1353
1354     def edit_account_label(self, k):
1355         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1356         if ok:
1357             label = unicode(text)
1358             self.wallet.set_label(k,label)
1359             self.update_address_tab()
1360
1361     def account_set_expanded(self, item, k, b):
1362         item.setExpanded(b)
1363         self.accounts_expanded[k] = b
1364
1365     def create_account_menu(self, position, k, item):
1366         menu = QMenu()
1367         if item.isExpanded():
1368             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1369         else:
1370             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1371         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1372         if self.wallet.seed_version > 4:
1373             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1374         if self.wallet.account_is_pending(k):
1375             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1376         menu.exec_(self.address_list.viewport().mapToGlobal(position))
1377
1378     def delete_pending_account(self, k):
1379         self.wallet.delete_pending_account(k)
1380         self.update_address_tab()
1381
1382     def create_receive_menu(self, position):
1383         # fixme: this function apparently has a side effect.
1384         # if it is not called the menu pops up several times
1385         #self.address_list.selectedIndexes()
1386
1387         selected = self.address_list.selectedItems()
1388         multi_select = len(selected) > 1
1389         addrs = [unicode(item.text(0)) for item in selected]
1390         if not multi_select:
1391             item = self.address_list.itemAt(position)
1392             if not item: return
1393
1394             addr = addrs[0]
1395             if not is_valid(addr):
1396                 k = str(item.data(0,32).toString())
1397                 if k:
1398                     self.create_account_menu(position, k, item)
1399                 else:
1400                     item.setExpanded(not item.isExpanded())
1401                 return
1402
1403         menu = QMenu()
1404         if not multi_select:
1405             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1406             menu.addAction(_("Request payment"), lambda: self.receive_at(addr))
1407             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1408             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1409             if not self.wallet.is_watching_only():
1410                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1411                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1412                 menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1413             if self.wallet.is_imported(addr):
1414                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1415
1416         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1417             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1418         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1419             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1420
1421         def can_send(addr):
1422             return addr not in self.wallet.frozen_addresses and self.wallet.get_addr_balance(addr) != (0, 0)
1423         if any(can_send(addr) for addr in addrs):
1424             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1425
1426         run_hook('receive_menu', menu, addrs)
1427         menu.exec_(self.address_list.viewport().mapToGlobal(position))
1428
1429
1430     def get_sendable_balance(self):
1431         return sum(map(lambda x:x['value'], self.get_coins()))
1432
1433
1434     def get_coins(self):
1435         if self.pay_from:
1436             return self.pay_from
1437         else:
1438             domain = self.wallet.get_account_addresses(self.current_account)
1439             for i in self.wallet.frozen_addresses:
1440                 if i in domain: domain.remove(i)
1441             return self.wallet.get_unspent_coins(domain)
1442
1443
1444     def send_from_addresses(self, addrs):
1445         self.set_pay_from( addrs )
1446         self.tabs.setCurrentIndex(1)
1447
1448
1449     def payto(self, addr):
1450         if not addr: return
1451         label = self.wallet.labels.get(addr)
1452         m_addr = label + '  <' + addr + '>' if label else addr
1453         self.tabs.setCurrentIndex(1)
1454         self.payto_e.setText(m_addr)
1455         self.amount_e.setFocus()
1456
1457
1458     def delete_contact(self, x):
1459         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1460             self.wallet.delete_contact(x)
1461             self.wallet.set_label(x, None)
1462             self.update_history_tab()
1463             self.update_contacts_tab()
1464             self.update_completions()
1465
1466
1467     def create_contact_menu(self, position):
1468         item = self.contacts_list.itemAt(position)
1469         menu = QMenu()
1470         if not item:
1471             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1472         else:
1473             addr = unicode(item.text(0))
1474             label = unicode(item.text(1))
1475             is_editable = item.data(0,32).toBool()
1476             payto_addr = item.data(0,33).toString()
1477             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1478             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1479             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1480             if is_editable:
1481                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1482                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1483
1484         run_hook('create_contact_menu', menu, item)
1485         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1486
1487     def delete_invoice(self, key):
1488         self.invoices.pop(key)
1489         self.wallet.storage.put('invoices', self.invoices)
1490         self.update_invoices_tab()
1491
1492     def show_invoice(self, key):
1493         from electrum.paymentrequest import PaymentRequest
1494         domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1495         pr = PaymentRequest(self.config)
1496         pr.read_file(key)
1497         pr.domain = domain
1498         pr.verify()
1499         self.show_pr_details(pr)
1500
1501     def show_pr_details(self, pr):
1502         msg = 'Domain: ' + pr.domain
1503         msg += '\nStatus: ' + pr.get_status()
1504         msg += '\nMemo: ' + pr.get_memo()
1505         msg += '\nPayment URL: ' + pr.payment_url
1506         msg += '\n\nOutputs:\n' + '\n'.join(map(lambda x: x[0] + ' ' + self.format_amount(x[1])+ self.base_unit(), pr.get_outputs()))
1507         QMessageBox.information(self, 'Invoice', msg , 'OK')
1508
1509     def do_pay_invoice(self, key):
1510         from electrum.paymentrequest import PaymentRequest
1511         domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1512         pr = PaymentRequest(self.config)
1513         pr.read_file(key)
1514         pr.domain = domain
1515         self.payment_request = pr
1516         self.prepare_for_payment_request()
1517         if pr.verify():
1518             self.payment_request_ok()
1519         else:
1520             self.payment_request_error()
1521             
1522
1523     def create_invoice_menu(self, position):
1524         item = self.invoices_list.itemAt(position)
1525         if not item:
1526             return
1527         k = self.invoices_list.indexOfTopLevelItem(item)
1528         key = self.invoices.keys()[k]
1529         domain, memo, value, expiration, status, tx_hash = self.invoices[key]
1530         menu = QMenu()
1531         menu.addAction(_("Details"), lambda: self.show_invoice(key))
1532         if status == PR_UNPAID:
1533             menu.addAction(_("Pay Now"), lambda: self.do_pay_invoice(key))
1534         menu.addAction(_("Delete"), lambda: self.delete_invoice(key))
1535         menu.exec_(self.invoices_list.viewport().mapToGlobal(position))
1536
1537
1538
1539     def update_address_tab(self):
1540         l = self.address_list
1541         # extend the syntax for consistency
1542         l.addChild = l.addTopLevelItem
1543         l.insertChild = l.insertTopLevelItem
1544
1545         l.clear()
1546
1547         accounts = self.wallet.get_accounts()
1548         if self.current_account is None:
1549             account_items = sorted(accounts.items())
1550         else:
1551             account_items = [(self.current_account, accounts.get(self.current_account))]
1552
1553
1554         for k, account in account_items:
1555
1556             if len(accounts) > 1:
1557                 name = self.wallet.get_account_name(k)
1558                 c,u = self.wallet.get_account_balance(k)
1559                 account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1560                 l.addTopLevelItem(account_item)
1561                 account_item.setExpanded(self.accounts_expanded.get(k, True))
1562                 account_item.setData(0, 32, k)
1563             else:
1564                 account_item = l
1565
1566             sequences = [0,1] if account.has_change() else [0]
1567             for is_change in sequences:
1568                 if len(sequences) > 1:
1569                     name = _("Receiving") if not is_change else _("Change")
1570                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1571                     account_item.addChild(seq_item)
1572                     if not is_change: 
1573                         seq_item.setExpanded(True)
1574                 else:
1575                     seq_item = account_item
1576                     
1577                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1578                 used_flag = False
1579
1580                 addr_list = account.get_addresses(is_change)
1581                 for address in addr_list:
1582                     num, is_used = self.wallet.is_used(address)
1583                     label = self.wallet.labels.get(address,'')
1584                     c, u = self.wallet.get_addr_balance(address)
1585                     balance = self.format_amount(c + u)
1586                     item = QTreeWidgetItem( [ address, label, balance, "%d"%num] )
1587                     item.setFont(0, QFont(MONOSPACE_FONT))
1588                     item.setData(0, 32, True) # label can be edited
1589                     if address in self.wallet.frozen_addresses:
1590                         item.setBackgroundColor(0, QColor('lightblue'))
1591                     if self.wallet.is_beyond_limit(address, account, is_change):
1592                         item.setBackgroundColor(0, QColor('red'))
1593                     if is_used:
1594                         if not used_flag:
1595                             seq_item.insertChild(0, used_item)
1596                             used_flag = True
1597                         used_item.addChild(item)
1598                     else:
1599                         seq_item.addChild(item)
1600
1601         # we use column 1 because column 0 may be hidden
1602         l.setCurrentItem(l.topLevelItem(0),1)
1603
1604
1605     def update_contacts_tab(self):
1606         l = self.contacts_list
1607         l.clear()
1608
1609         for address in self.wallet.addressbook:
1610             label = self.wallet.labels.get(address,'')
1611             n = self.wallet.get_num_tx(address)
1612             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1613             item.setFont(0, QFont(MONOSPACE_FONT))
1614             # 32 = label can be edited (bool)
1615             item.setData(0,32, True)
1616             # 33 = payto string
1617             item.setData(0,33, address)
1618             l.addTopLevelItem(item)
1619
1620         run_hook('update_contacts_tab', l)
1621         l.setCurrentItem(l.topLevelItem(0))
1622
1623
1624     def create_console_tab(self):
1625         from console import Console
1626         self.console = console = Console()
1627         return console
1628
1629
1630     def update_console(self):
1631         console = self.console
1632         console.history = self.config.get("console-history",[])
1633         console.history_index = len(console.history)
1634
1635         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1636         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1637
1638         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1639         methods = {}
1640         def mkfunc(f, method):
1641             return lambda *args: apply( f, (method, args, self.password_dialog ))
1642         for m in dir(c):
1643             if m[0]=='_' or m in ['network','wallet']: continue
1644             methods[m] = mkfunc(c._run, m)
1645
1646         console.updateNamespace(methods)
1647
1648
1649     def change_account(self,s):
1650         if s == _("All accounts"):
1651             self.current_account = None
1652         else:
1653             accounts = self.wallet.get_account_names()
1654             for k, v in accounts.items():
1655                 if v == s:
1656                     self.current_account = k
1657         self.update_history_tab()
1658         self.update_status()
1659         self.update_address_tab()
1660         self.update_receive_tab()
1661
1662     def create_status_bar(self):
1663
1664         sb = QStatusBar()
1665         sb.setFixedHeight(35)
1666         qtVersion = qVersion()
1667
1668         self.balance_label = QLabel("")
1669         sb.addWidget(self.balance_label)
1670
1671         from version_getter import UpdateLabel
1672         self.updatelabel = UpdateLabel(self.config, sb)
1673
1674         self.account_selector = QComboBox()
1675         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1676         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1677         sb.addPermanentWidget(self.account_selector)
1678
1679         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1680             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1681
1682         self.lock_icon = QIcon()
1683         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1684         sb.addPermanentWidget( self.password_button )
1685
1686         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1687         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1688         sb.addPermanentWidget( self.seed_button )
1689         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1690         sb.addPermanentWidget( self.status_button )
1691
1692         run_hook('create_status_bar', (sb,))
1693
1694         self.setStatusBar(sb)
1695
1696
1697     def update_lock_icon(self):
1698         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1699         self.password_button.setIcon( icon )
1700
1701
1702     def update_buttons_on_seed(self):
1703         if self.wallet.has_seed():
1704            self.seed_button.show()
1705         else:
1706            self.seed_button.hide()
1707
1708         if not self.wallet.is_watching_only():
1709            self.password_button.show()
1710            self.send_button.setText(_("Send"))
1711         else:
1712            self.password_button.hide()
1713            self.send_button.setText(_("Create unsigned transaction"))
1714
1715
1716     def change_password_dialog(self):
1717         from password_dialog import PasswordDialog
1718         d = PasswordDialog(self.wallet, self)
1719         d.run()
1720         self.update_lock_icon()
1721
1722
1723     def new_contact_dialog(self):
1724
1725         d = QDialog(self)
1726         d.setWindowTitle(_("New Contact"))
1727         vbox = QVBoxLayout(d)
1728         vbox.addWidget(QLabel(_('New Contact')+':'))
1729
1730         grid = QGridLayout()
1731         line1 = QLineEdit()
1732         line2 = QLineEdit()
1733         grid.addWidget(QLabel(_("Address")), 1, 0)
1734         grid.addWidget(line1, 1, 1)
1735         grid.addWidget(QLabel(_("Name")), 2, 0)
1736         grid.addWidget(line2, 2, 1)
1737
1738         vbox.addLayout(grid)
1739         vbox.addLayout(ok_cancel_buttons(d))
1740
1741         if not d.exec_():
1742             return
1743
1744         address = str(line1.text())
1745         label = unicode(line2.text())
1746
1747         if not is_valid(address):
1748             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1749             return
1750
1751         self.wallet.add_contact(address)
1752         if label:
1753             self.wallet.set_label(address, label)
1754
1755         self.update_contacts_tab()
1756         self.update_history_tab()
1757         self.update_completions()
1758         self.tabs.setCurrentIndex(3)
1759
1760
1761     @protected
1762     def new_account_dialog(self, password):
1763
1764         dialog = QDialog(self)
1765         dialog.setModal(1)
1766         dialog.setWindowTitle(_("New Account"))
1767
1768         vbox = QVBoxLayout()
1769         vbox.addWidget(QLabel(_('Account name')+':'))
1770         e = QLineEdit()
1771         vbox.addWidget(e)
1772         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1773             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1774         l = QLabel(msg)
1775         l.setWordWrap(True)
1776         vbox.addWidget(l)
1777
1778         vbox.addLayout(ok_cancel_buttons(dialog))
1779         dialog.setLayout(vbox)
1780         r = dialog.exec_()
1781         if not r: return
1782
1783         name = str(e.text())
1784         if not name: return
1785
1786         self.wallet.create_pending_account(name, password)
1787         self.update_address_tab()
1788         self.tabs.setCurrentIndex(3)
1789
1790
1791
1792
1793     def show_master_public_keys(self):
1794
1795         dialog = QDialog(self)
1796         dialog.setModal(1)
1797         dialog.setWindowTitle(_("Master Public Keys"))
1798
1799         main_layout = QGridLayout()
1800         mpk_dict = self.wallet.get_master_public_keys()
1801         i = 0
1802         for key, value in mpk_dict.items():
1803             main_layout.addWidget(QLabel(key), i, 0)
1804             mpk_text = QTextEdit()
1805             mpk_text.setReadOnly(True)
1806             mpk_text.setMaximumHeight(170)
1807             mpk_text.setText(value)
1808             main_layout.addWidget(mpk_text, i + 1, 0)
1809             i += 2
1810
1811         vbox = QVBoxLayout()
1812         vbox.addLayout(main_layout)
1813         vbox.addLayout(close_button(dialog))
1814
1815         dialog.setLayout(vbox)
1816         dialog.exec_()
1817
1818
1819     @protected
1820     def show_seed_dialog(self, password):
1821         if not self.wallet.has_seed():
1822             QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
1823             return
1824
1825         try:
1826             mnemonic = self.wallet.get_mnemonic(password)
1827         except Exception:
1828             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1829             return
1830         from seed_dialog import SeedDialog
1831         d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
1832         d.exec_()
1833
1834
1835
1836     def show_qrcode(self, data, title = _("QR code")):
1837         if not data: 
1838             return
1839         d = QRDialog(data, self, title)
1840         d.exec_()
1841
1842
1843     def do_protect(self, func, args):
1844         if self.wallet.use_encryption:
1845             password = self.password_dialog()
1846             if not password:
1847                 return
1848         else:
1849             password = None
1850
1851         if args != (False,):
1852             args = (self,) + args + (password,)
1853         else:
1854             args = (self,password)
1855         apply( func, args)
1856
1857
1858     def show_public_keys(self, address):
1859         if not address: return
1860         try:
1861             pubkey_list = self.wallet.get_public_keys(address)
1862         except Exception as e:
1863             traceback.print_exc(file=sys.stdout)
1864             self.show_message(str(e))
1865             return
1866
1867         d = QDialog(self)
1868         d.setMinimumSize(600, 200)
1869         d.setModal(1)
1870         vbox = QVBoxLayout()
1871         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1872         vbox.addWidget( QLabel(_("Public key") + ':'))
1873         keys = QRTextEdit()
1874         keys.setReadOnly(True)
1875         keys.setText('\n'.join(pubkey_list))
1876         vbox.addWidget(keys)
1877         vbox.addLayout(close_button(d))
1878         d.setLayout(vbox)
1879         d.exec_()
1880
1881     @protected
1882     def show_private_key(self, address, password):
1883         if not address: return
1884         try:
1885             pk_list = self.wallet.get_private_key(address, password)
1886         except Exception as e:
1887             traceback.print_exc(file=sys.stdout)
1888             self.show_message(str(e))
1889             return
1890
1891         d = QDialog(self)
1892         d.setMinimumSize(600, 200)
1893         d.setModal(1)
1894         vbox = QVBoxLayout()
1895         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1896         vbox.addWidget( QLabel(_("Private key") + ':'))
1897         keys = QRTextEdit()
1898         keys.setReadOnly(True)
1899         keys.setText('\n'.join(pk_list))
1900         vbox.addWidget(keys)
1901         vbox.addLayout(close_button(d))
1902         d.setLayout(vbox)
1903         d.exec_()
1904
1905
1906     @protected
1907     def do_sign(self, address, message, signature, password):
1908         message = unicode(message.toPlainText())
1909         message = message.encode('utf-8')
1910         try:
1911             sig = self.wallet.sign_message(str(address.text()), message, password)
1912             signature.setText(sig)
1913         except Exception as e:
1914             self.show_message(str(e))
1915
1916     def do_verify(self, address, message, signature):
1917         message = unicode(message.toPlainText())
1918         message = message.encode('utf-8')
1919         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1920             self.show_message(_("Signature verified"))
1921         else:
1922             self.show_message(_("Error: wrong signature"))
1923
1924
1925     def sign_verify_message(self, address=''):
1926         d = QDialog(self)
1927         d.setModal(1)
1928         d.setWindowTitle(_('Sign/verify Message'))
1929         d.setMinimumSize(410, 290)
1930
1931         layout = QGridLayout(d)
1932
1933         message_e = QTextEdit()
1934         layout.addWidget(QLabel(_('Message')), 1, 0)
1935         layout.addWidget(message_e, 1, 1)
1936         layout.setRowStretch(2,3)
1937
1938         address_e = QLineEdit()
1939         address_e.setText(address)
1940         layout.addWidget(QLabel(_('Address')), 2, 0)
1941         layout.addWidget(address_e, 2, 1)
1942
1943         signature_e = QTextEdit()
1944         layout.addWidget(QLabel(_('Signature')), 3, 0)
1945         layout.addWidget(signature_e, 3, 1)
1946         layout.setRowStretch(3,1)
1947
1948         hbox = QHBoxLayout()
1949
1950         b = QPushButton(_("Sign"))
1951         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1952         hbox.addWidget(b)
1953
1954         b = QPushButton(_("Verify"))
1955         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1956         hbox.addWidget(b)
1957
1958         b = QPushButton(_("Close"))
1959         b.clicked.connect(d.accept)
1960         hbox.addWidget(b)
1961         layout.addLayout(hbox, 4, 1)
1962         d.exec_()
1963
1964
1965     @protected
1966     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1967         try:
1968             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1969             message_e.setText(decrypted)
1970         except Exception as e:
1971             self.show_message(str(e))
1972
1973
1974     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1975         message = unicode(message_e.toPlainText())
1976         message = message.encode('utf-8')
1977         try:
1978             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1979             encrypted_e.setText(encrypted)
1980         except Exception as e:
1981             self.show_message(str(e))
1982
1983
1984
1985     def encrypt_message(self, address = ''):
1986         d = QDialog(self)
1987         d.setModal(1)
1988         d.setWindowTitle(_('Encrypt/decrypt Message'))
1989         d.setMinimumSize(610, 490)
1990
1991         layout = QGridLayout(d)
1992
1993         message_e = QTextEdit()
1994         layout.addWidget(QLabel(_('Message')), 1, 0)
1995         layout.addWidget(message_e, 1, 1)
1996         layout.setRowStretch(2,3)
1997
1998         pubkey_e = QLineEdit()
1999         if address:
2000             pubkey = self.wallet.get_public_keys(address)[0]
2001             pubkey_e.setText(pubkey)
2002         layout.addWidget(QLabel(_('Public key')), 2, 0)
2003         layout.addWidget(pubkey_e, 2, 1)
2004
2005         encrypted_e = QTextEdit()
2006         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
2007         layout.addWidget(encrypted_e, 3, 1)
2008         layout.setRowStretch(3,1)
2009
2010         hbox = QHBoxLayout()
2011         b = QPushButton(_("Encrypt"))
2012         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
2013         hbox.addWidget(b)
2014
2015         b = QPushButton(_("Decrypt"))
2016         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
2017         hbox.addWidget(b)
2018
2019         b = QPushButton(_("Close"))
2020         b.clicked.connect(d.accept)
2021         hbox.addWidget(b)
2022
2023         layout.addLayout(hbox, 4, 1)
2024         d.exec_()
2025
2026
2027     def question(self, msg):
2028         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
2029
2030     def show_message(self, msg):
2031         QMessageBox.information(self, _('Message'), msg, _('OK'))
2032
2033     def password_dialog(self, msg=None):
2034         d = QDialog(self)
2035         d.setModal(1)
2036         d.setWindowTitle(_("Enter Password"))
2037
2038         pw = QLineEdit()
2039         pw.setEchoMode(2)
2040
2041         vbox = QVBoxLayout()
2042         if not msg:
2043             msg = _('Please enter your password')
2044         vbox.addWidget(QLabel(msg))
2045
2046         grid = QGridLayout()
2047         grid.setSpacing(8)
2048         grid.addWidget(QLabel(_('Password')), 1, 0)
2049         grid.addWidget(pw, 1, 1)
2050         vbox.addLayout(grid)
2051
2052         vbox.addLayout(ok_cancel_buttons(d))
2053         d.setLayout(vbox)
2054
2055         run_hook('password_dialog', pw, grid, 1)
2056         if not d.exec_(): return
2057         return unicode(pw.text())
2058
2059
2060
2061
2062
2063
2064
2065
2066     def tx_from_text(self, txt):
2067         "json or raw hexadecimal"
2068         try:
2069             txt.decode('hex')
2070             is_hex = True
2071         except:
2072             is_hex = False
2073
2074         if is_hex:
2075             try:
2076                 return Transaction.deserialize(txt)
2077             except:
2078                 traceback.print_exc(file=sys.stdout)
2079                 QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2080                 return
2081
2082         try:
2083             tx_dict = json.loads(str(txt))
2084             assert "hex" in tx_dict.keys()
2085             tx = Transaction.deserialize(tx_dict["hex"])
2086             #if tx_dict.has_key("input_info"):
2087             #    input_info = json.loads(tx_dict['input_info'])
2088             #    tx.add_input_info(input_info)
2089             return tx
2090         except Exception:
2091             traceback.print_exc(file=sys.stdout)
2092             QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
2093
2094
2095     def read_tx_from_qrcode(self):
2096         data = run_hook('scan_qr_hook')
2097         if not data:
2098             return
2099         # transactions are binary, but qrcode seems to return utf8...
2100         z = data.decode('utf8')
2101         s = ''
2102         for b in z:
2103             s += chr(ord(b))
2104         data = s.encode('hex')
2105         tx = self.tx_from_text(data)
2106         if not tx:
2107             return
2108         self.show_transaction(tx)
2109
2110
2111     def read_tx_from_file(self):
2112         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
2113         if not fileName:
2114             return
2115         try:
2116             with open(fileName, "r") as f:
2117                 file_content = f.read()
2118         except (ValueError, IOError, os.error), reason:
2119             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2120
2121         return self.tx_from_text(file_content)
2122
2123
2124     @protected
2125     def sign_raw_transaction(self, tx, password):
2126         try:
2127             self.wallet.signrawtransaction(tx, [], password)
2128         except Exception as e:
2129             traceback.print_exc(file=sys.stdout)
2130             QMessageBox.warning(self, _("Error"), str(e))
2131
2132     def do_process_from_text(self):
2133         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
2134         if not text:
2135             return
2136         tx = self.tx_from_text(text)
2137         if tx:
2138             self.show_transaction(tx)
2139
2140     def do_process_from_file(self):
2141         tx = self.read_tx_from_file()
2142         if tx:
2143             self.show_transaction(tx)
2144
2145     def do_process_from_txid(self):
2146         from electrum import transaction
2147         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
2148         if ok and txid:
2149             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
2150             if r:
2151                 tx = transaction.Transaction.deserialize(r)
2152                 if tx:
2153                     self.show_transaction(tx)
2154                 else:
2155                     self.show_message("unknown transaction")
2156
2157     def do_process_from_csvReader(self, csvReader):
2158         outputs = []
2159         errors = []
2160         errtext = ""
2161         try:
2162             for position, row in enumerate(csvReader):
2163                 address = row[0]
2164                 if not is_address(address):
2165                     errors.append((position, address))
2166                     continue
2167                 amount = Decimal(row[1])
2168                 amount = int(100000000*amount)
2169                 outputs.append(('address', address, amount))
2170         except (ValueError, IOError, os.error), reason:
2171             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2172             return
2173         if errors != []:
2174             for x in errors:
2175                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
2176             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
2177             return
2178
2179         try:
2180             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
2181         except Exception as e:
2182             self.show_message(str(e))
2183             return
2184
2185         self.show_transaction(tx)
2186
2187     def do_process_from_csv_file(self):
2188         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
2189         if not fileName:
2190             return
2191         try:
2192             with open(fileName, "r") as f:
2193                 csvReader = csv.reader(f)
2194                 self.do_process_from_csvReader(csvReader)
2195         except (ValueError, IOError, os.error), reason:
2196             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2197             return
2198
2199     def do_process_from_csv_text(self):
2200         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2201                                + _("Format: address, amount. One output per line"), _("Load CSV"))
2202         if not text:
2203             return
2204         f = StringIO.StringIO(text)
2205         csvReader = csv.reader(f)
2206         self.do_process_from_csvReader(csvReader)
2207
2208
2209
2210     @protected
2211     def export_privkeys_dialog(self, password):
2212         if self.wallet.is_watching_only():
2213             self.show_message(_("This is a watching-only wallet"))
2214             return
2215
2216         d = QDialog(self)
2217         d.setWindowTitle(_('Private keys'))
2218         d.setMinimumSize(850, 300)
2219         vbox = QVBoxLayout(d)
2220
2221         msg = "%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."), 
2222                               _("Exposing a single private key can compromise your entire wallet!"), 
2223                               _("In particular, DO NOT use 'redeem private key' services proposed by third parties."))
2224         vbox.addWidget(QLabel(msg))
2225
2226         e = QTextEdit()
2227         e.setReadOnly(True)
2228         vbox.addWidget(e)
2229
2230         defaultname = 'electrum-private-keys.csv'
2231         select_msg = _('Select file to export your private keys to')
2232         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2233         vbox.addLayout(hbox)
2234
2235         h, b = ok_cancel_buttons2(d, _('Export'))
2236         b.setEnabled(False)
2237         vbox.addLayout(h)
2238
2239         private_keys = {}
2240         addresses = self.wallet.addresses(True)
2241         done = False
2242         def privkeys_thread():
2243             for addr in addresses:
2244                 time.sleep(0.1)
2245                 if done: 
2246                     break
2247                 private_keys[addr] = "\n".join(self.wallet.get_private_key(addr, password))
2248                 d.emit(SIGNAL('computing_privkeys'))
2249             d.emit(SIGNAL('show_privkeys'))
2250
2251         def show_privkeys():
2252             s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
2253             e.setText(s)
2254             b.setEnabled(True)
2255
2256         d.connect(d, QtCore.SIGNAL('computing_privkeys'), lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
2257         d.connect(d, QtCore.SIGNAL('show_privkeys'), show_privkeys)
2258         threading.Thread(target=privkeys_thread).start()
2259
2260         if not d.exec_():
2261             done = True
2262             return
2263
2264         filename = filename_e.text()
2265         if not filename:
2266             return
2267
2268         try:
2269             self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
2270         except (IOError, os.error), reason:
2271             export_error_label = _("Electrum was unable to produce a private key-export.")
2272             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2273
2274         except Exception as e:
2275             self.show_message(str(e))
2276             return
2277
2278         self.show_message(_("Private keys exported."))
2279
2280
2281     def do_export_privkeys(self, fileName, pklist, is_csv):
2282         with open(fileName, "w+") as f:
2283             if is_csv:
2284                 transaction = csv.writer(f)
2285                 transaction.writerow(["address", "private_key"])
2286                 for addr, pk in pklist.items():
2287                     transaction.writerow(["%34s"%addr,pk])
2288             else:
2289                 import json
2290                 f.write(json.dumps(pklist, indent = 4))
2291
2292
2293     def do_import_labels(self):
2294         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2295         if not labelsFile: return
2296         try:
2297             f = open(labelsFile, 'r')
2298             data = f.read()
2299             f.close()
2300             for key, value in json.loads(data).items():
2301                 self.wallet.set_label(key, value)
2302             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2303         except (IOError, os.error), reason:
2304             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2305
2306
2307     def do_export_labels(self):
2308         labels = self.wallet.labels
2309         try:
2310             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2311             if fileName:
2312                 with open(fileName, 'w+') as f:
2313                     json.dump(labels, f)
2314                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2315         except (IOError, os.error), reason:
2316             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2317
2318
2319     def export_history_dialog(self):
2320
2321         d = QDialog(self)
2322         d.setWindowTitle(_('Export History'))
2323         d.setMinimumSize(400, 200)
2324         vbox = QVBoxLayout(d)
2325
2326         defaultname = os.path.expanduser('~/electrum-history.csv')
2327         select_msg = _('Select file to export your wallet transactions to')
2328
2329         hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
2330         vbox.addLayout(hbox)
2331
2332         vbox.addStretch(1)
2333
2334         h, b = ok_cancel_buttons2(d, _('Export'))
2335         vbox.addLayout(h)
2336         if not d.exec_():
2337             return
2338
2339         filename = filename_e.text()
2340         if not filename:
2341             return
2342
2343         try:
2344             self.do_export_history(self.wallet, filename, csv_button.isChecked())
2345         except (IOError, os.error), reason:
2346             export_error_label = _("Electrum was unable to produce a transaction export.")
2347             QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
2348             return
2349
2350         QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
2351
2352
2353     def do_export_history(self, wallet, fileName, is_csv):
2354         history = wallet.get_tx_history()
2355         lines = []
2356         for item in history:
2357             tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
2358             if confirmations:
2359                 if timestamp is not None:
2360                     try:
2361                         time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
2362                     except [RuntimeError, TypeError, NameError] as reason:
2363                         time_string = "unknown"
2364                         pass
2365                 else:
2366                     time_string = "unknown"
2367             else:
2368                 time_string = "pending"
2369
2370             if value is not None:
2371                 value_string = format_satoshis(value, True)
2372             else:
2373                 value_string = '--'
2374
2375             if fee is not None:
2376                 fee_string = format_satoshis(fee, True)
2377             else:
2378                 fee_string = '0'
2379
2380             if tx_hash:
2381                 label, is_default_label = wallet.get_label(tx_hash)
2382                 label = label.encode('utf-8')
2383             else:
2384                 label = ""
2385
2386             balance_string = format_satoshis(balance, False)
2387             if is_csv:
2388                 lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
2389             else:
2390                 lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
2391
2392         with open(fileName, "w+") as f:
2393             if is_csv:
2394                 transaction = csv.writer(f)
2395                 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
2396                 for line in lines:
2397                     transaction.writerow(line)
2398             else:
2399                 import json
2400                 f.write(json.dumps(lines, indent = 4))
2401
2402
2403     def sweep_key_dialog(self):
2404         d = QDialog(self)
2405         d.setWindowTitle(_('Sweep private keys'))
2406         d.setMinimumSize(600, 300)
2407
2408         vbox = QVBoxLayout(d)
2409         vbox.addWidget(QLabel(_("Enter private keys")))
2410
2411         keys_e = QTextEdit()
2412         keys_e.setTabChangesFocus(True)
2413         vbox.addWidget(keys_e)
2414
2415         h, address_e = address_field(self.wallet.addresses())
2416         vbox.addLayout(h)
2417
2418         vbox.addStretch(1)
2419         hbox, button = ok_cancel_buttons2(d, _('Sweep'))
2420         vbox.addLayout(hbox)
2421         button.setEnabled(False)
2422
2423         def get_address():
2424             addr = str(address_e.text())
2425             if bitcoin.is_address(addr):
2426                 return addr
2427
2428         def get_pk():
2429             pk = str(keys_e.toPlainText()).strip()
2430             if Wallet.is_private_key(pk):
2431                 return pk.split()
2432
2433         f = lambda: button.setEnabled(get_address() is not None and get_pk() is not None)
2434         keys_e.textChanged.connect(f)
2435         address_e.textChanged.connect(f)
2436         if not d.exec_():
2437             return
2438
2439         fee = self.wallet.fee
2440         tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
2441         self.show_transaction(tx)
2442
2443
2444     @protected
2445     def do_import_privkey(self, password):
2446         if not self.wallet.has_imported_keys():
2447             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2448                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2449                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2450             if r == 4: return
2451
2452         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2453         if not text: return
2454
2455         text = str(text).split()
2456         badkeys = []
2457         addrlist = []
2458         for key in text:
2459             try:
2460                 addr = self.wallet.import_key(key, password)
2461             except Exception as e:
2462                 badkeys.append(key)
2463                 continue
2464             if not addr:
2465                 badkeys.append(key)
2466             else:
2467                 addrlist.append(addr)
2468         if addrlist:
2469             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2470         if badkeys:
2471             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2472         self.update_address_tab()
2473         self.update_history_tab()
2474
2475
2476     def settings_dialog(self):
2477         d = QDialog(self)
2478         d.setWindowTitle(_('Electrum Settings'))
2479         d.setModal(1)
2480         vbox = QVBoxLayout()
2481         grid = QGridLayout()
2482         grid.setColumnStretch(0,1)
2483
2484         nz_label = QLabel(_('Display zeros') + ':')
2485         grid.addWidget(nz_label, 0, 0)
2486         nz_e = AmountEdit(None,True)
2487         nz_e.setText("%d"% self.num_zeros)
2488         grid.addWidget(nz_e, 0, 1)
2489         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2490         grid.addWidget(HelpButton(msg), 0, 2)
2491         if not self.config.is_modifiable('num_zeros'):
2492             for w in [nz_e, nz_label]: w.setEnabled(False)
2493
2494         lang_label=QLabel(_('Language') + ':')
2495         grid.addWidget(lang_label, 1, 0)
2496         lang_combo = QComboBox()
2497         from electrum.i18n import languages
2498         lang_combo.addItems(languages.values())
2499         try:
2500             index = languages.keys().index(self.config.get("language",''))
2501         except Exception:
2502             index = 0
2503         lang_combo.setCurrentIndex(index)
2504         grid.addWidget(lang_combo, 1, 1)
2505         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2506         if not self.config.is_modifiable('language'):
2507             for w in [lang_combo, lang_label]: w.setEnabled(False)
2508
2509
2510         fee_label = QLabel(_('Transaction fee') + ':')
2511         grid.addWidget(fee_label, 2, 0)
2512         fee_e = BTCAmountEdit(self.get_decimal_point)
2513         fee_e.setAmount(self.wallet.fee)
2514         grid.addWidget(fee_e, 2, 1)
2515         msg = _('Fee per kilobyte of transaction.') + '\n' \
2516             + _('Recommended value') + ': ' + self.format_amount(10000) + ' ' + self.base_unit()
2517         grid.addWidget(HelpButton(msg), 2, 2)
2518         if not self.config.is_modifiable('fee_per_kb'):
2519             for w in [fee_e, fee_label]: w.setEnabled(False)
2520
2521         units = ['BTC', 'mBTC', 'bits']
2522         unit_label = QLabel(_('Base unit') + ':')
2523         grid.addWidget(unit_label, 3, 0)
2524         unit_combo = QComboBox()
2525         unit_combo.addItems(units)
2526         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2527         grid.addWidget(unit_combo, 3, 1)
2528         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2529                                              + '\n1BTC=1000mBTC.\n' \
2530                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2531
2532         usechange_cb = QCheckBox(_('Use change addresses'))
2533         usechange_cb.setChecked(self.wallet.use_change)
2534         grid.addWidget(usechange_cb, 4, 0)
2535         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2536         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2537
2538         block_explorers = ['Blockchain.info', 'Blockr.io', 'Insight.is']
2539         block_ex_label = QLabel(_('Online Block Explorer') + ':')
2540         grid.addWidget(block_ex_label, 5, 0)
2541         block_ex_combo = QComboBox()
2542         block_ex_combo.addItems(block_explorers)
2543         block_ex_combo.setCurrentIndex(block_explorers.index(self.config.get('block_explorer', 'Blockchain.info')))
2544         grid.addWidget(block_ex_combo, 5, 1)
2545         grid.addWidget(HelpButton(_('Choose which online block explorer to use for functions that open a web browser')+' '), 5, 2)
2546
2547         show_tx = self.config.get('show_before_broadcast', False)
2548         showtx_cb = QCheckBox(_('Show before broadcast'))
2549         showtx_cb.setChecked(show_tx)
2550         grid.addWidget(showtx_cb, 6, 0)
2551         grid.addWidget(HelpButton(_('Display the details of your transactions before broadcasting it.')), 6, 2)
2552
2553         vbox.addLayout(grid)
2554         vbox.addStretch(1)
2555         vbox.addLayout(ok_cancel_buttons(d))
2556         d.setLayout(vbox)
2557
2558         # run the dialog
2559         if not d.exec_(): return
2560
2561         fee = fee_e.get_amount()
2562         if fee is None:
2563             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2564             return
2565
2566         self.wallet.set_fee(fee)
2567
2568         nz = unicode(nz_e.text())
2569         try:
2570             nz = int( nz )
2571             if nz>8: nz=8
2572         except Exception:
2573             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2574             return
2575
2576         if self.num_zeros != nz:
2577             self.num_zeros = nz
2578             self.config.set_key('num_zeros', nz, True)
2579             self.update_history_tab()
2580             self.update_address_tab()
2581
2582         usechange_result = usechange_cb.isChecked()
2583         if self.wallet.use_change != usechange_result:
2584             self.wallet.use_change = usechange_result
2585             self.wallet.storage.put('use_change', self.wallet.use_change)
2586
2587         if showtx_cb.isChecked() != show_tx:
2588             self.config.set_key('show_before_broadcast', not show_tx)
2589
2590         unit_result = units[unit_combo.currentIndex()]
2591         if self.base_unit() != unit_result:
2592             if unit_result == 'BTC':
2593                 self.decimal_point = 8
2594             elif unit_result == 'mBTC':
2595                 self.decimal_point = 5
2596             elif unit_result == 'bits':
2597                 self.decimal_point = 2
2598             else:
2599                 raise Exception('Unknown base unit')
2600             self.config.set_key('decimal_point', self.decimal_point, True)
2601             self.update_history_tab()
2602             self.update_status()
2603
2604         need_restart = False
2605
2606         lang_request = languages.keys()[lang_combo.currentIndex()]
2607         if lang_request != self.config.get('language'):
2608             self.config.set_key("language", lang_request, True)
2609             need_restart = True
2610
2611         be_result = block_explorers[block_ex_combo.currentIndex()]
2612         self.config.set_key('block_explorer', be_result, True)
2613
2614         run_hook('close_settings_dialog')
2615
2616         if need_restart:
2617             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2618
2619
2620     def run_network_dialog(self):
2621         if not self.network:
2622             return
2623         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2624
2625     def closeEvent(self, event):
2626         self.tray.hide()
2627         self.config.set_key("is_maximized", self.isMaximized())
2628         if not self.isMaximized():
2629             g = self.geometry()
2630             self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
2631         self.save_column_widths()
2632         self.config.set_key("console-history", self.console.history[-50:], True)
2633         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2634         event.accept()
2635
2636
2637     def plugins_dialog(self):
2638         from electrum.plugins import plugins
2639
2640         d = QDialog(self)
2641         d.setWindowTitle(_('Electrum Plugins'))
2642         d.setModal(1)
2643
2644         vbox = QVBoxLayout(d)
2645
2646         # plugins
2647         scroll = QScrollArea()
2648         scroll.setEnabled(True)
2649         scroll.setWidgetResizable(True)
2650         scroll.setMinimumSize(400,250)
2651         vbox.addWidget(scroll)
2652
2653         w = QWidget()
2654         scroll.setWidget(w)
2655         w.setMinimumHeight(len(plugins)*35)
2656
2657         grid = QGridLayout()
2658         grid.setColumnStretch(0,1)
2659         w.setLayout(grid)
2660
2661         def do_toggle(cb, p, w):
2662             r = p.toggle()
2663             cb.setChecked(r)
2664             if w: w.setEnabled(r)
2665
2666         def mk_toggle(cb, p, w):
2667             return lambda: do_toggle(cb,p,w)
2668
2669         for i, p in enumerate(plugins):
2670             try:
2671                 cb = QCheckBox(p.fullname())
2672                 cb.setDisabled(not p.is_available())
2673                 cb.setChecked(p.is_enabled())
2674                 grid.addWidget(cb, i, 0)
2675                 if p.requires_settings():
2676                     w = p.settings_widget(self)
2677                     w.setEnabled( p.is_enabled() )
2678                     grid.addWidget(w, i, 1)
2679                 else:
2680                     w = None
2681                 cb.clicked.connect(mk_toggle(cb,p,w))
2682                 grid.addWidget(HelpButton(p.description()), i, 2)
2683             except Exception:
2684                 print_msg(_("Error: cannot display plugin"), p)
2685                 traceback.print_exc(file=sys.stdout)
2686         grid.setRowStretch(i+1,1)
2687
2688         vbox.addLayout(close_button(d))
2689
2690         d.exec_()
2691
2692
2693     def show_account_details(self, k):
2694         account = self.wallet.accounts[k]
2695
2696         d = QDialog(self)
2697         d.setWindowTitle(_('Account Details'))
2698         d.setModal(1)
2699
2700         vbox = QVBoxLayout(d)
2701         name = self.wallet.get_account_name(k)
2702         label = QLabel('Name: ' + name)
2703         vbox.addWidget(label)
2704
2705         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2706
2707         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2708
2709         vbox.addWidget(QLabel(_('Master Public Key:')))
2710
2711         text = QTextEdit()
2712         text.setReadOnly(True)
2713         text.setMaximumHeight(170)
2714         vbox.addWidget(text)
2715
2716         mpk_text = '\n'.join( account.get_master_pubkeys() )
2717         text.setText(mpk_text)
2718
2719         vbox.addLayout(close_button(d))
2720         d.exec_()