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