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