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