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