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