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