transaction: add_signature(), is_complete() methods
[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.wallet 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
44
45 from electrum import bmp, pyqrnative
46
47 from amountedit import AmountEdit
48 from network_dialog import NetworkDialog
49 from qrcodewidget import QRCodeWidget
50
51 from decimal import Decimal
52
53 import platform
54 import httplib
55 import socket
56 import webbrowser
57 import csv
58
59 if platform.system() == 'Windows':
60     MONOSPACE_FONT = 'Lucida Console'
61 elif platform.system() == 'Darwin':
62     MONOSPACE_FONT = 'Monaco'
63 else:
64     MONOSPACE_FONT = 'monospace'
65
66 from electrum import ELECTRUM_VERSION
67 import re
68
69 from util import *
70
71
72
73
74
75
76 class StatusBarButton(QPushButton):
77     def __init__(self, icon, tooltip, func):
78         QPushButton.__init__(self, icon, '')
79         self.setToolTip(tooltip)
80         self.setFlat(True)
81         self.setMaximumWidth(25)
82         self.clicked.connect(func)
83         self.func = func
84         self.setIconSize(QSize(25,25))
85
86     def keyPressEvent(self, e):
87         if e.key() == QtCore.Qt.Key_Return:
88             apply(self.func,())
89
90
91
92
93
94
95
96
97
98
99 default_column_widths = { "history":[40,140,350,140], "contacts":[350,330], "receive": [370,200,130] }
100
101 class ElectrumWindow(QMainWindow):
102     def build_menu(self):
103         m = QMenu()
104         m.addAction(_("Show/Hide"), self.show_or_hide)
105         m.addSeparator()
106         m.addAction(_("Exit Electrum"), self.close)
107         self.tray.setContextMenu(m)
108
109     def show_or_hide(self):
110         self.tray_activated(QSystemTrayIcon.DoubleClick)
111
112     def tray_activated(self, reason):
113         if reason == QSystemTrayIcon.DoubleClick:
114             if self.isMinimized() or self.isHidden():
115                 self.show()
116                 self.raise_()
117             else:
118                 self.hide()
119
120     def __init__(self, config, network):
121         QMainWindow.__init__(self)
122
123         self.config = config
124         self.network = network
125
126         self._close_electrum = False
127         self.lite = None
128
129         if sys.platform == 'darwin':
130           self.icon = QIcon(":icons/electrum_dark_icon.png")
131           #self.icon = QIcon(":icons/lock.png")
132         else:
133           self.icon = QIcon(':icons/electrum_light_icon.png')
134
135         self.tray = QSystemTrayIcon(self.icon, self)
136         self.tray.setToolTip('Electrum')
137         self.tray.activated.connect(self.tray_activated)
138
139         self.build_menu()
140         self.tray.show()
141         self.create_status_bar()
142
143         self.need_update = threading.Event()
144
145         self.decimal_point = config.get('decimal_point', 8)
146         self.num_zeros     = int(config.get('num_zeros',0))
147
148         set_language(config.get('language'))
149
150         self.funds_error = False
151         self.completions = QStringListModel()
152
153         self.tabs = tabs = QTabWidget(self)
154         self.column_widths = self.config.get("column_widths_2", default_column_widths )
155         tabs.addTab(self.create_history_tab(), _('History') )
156         tabs.addTab(self.create_send_tab(), _('Send') )
157         tabs.addTab(self.create_receive_tab(), _('Receive') )
158         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
159         tabs.addTab(self.create_console_tab(), _('Console') )
160         tabs.setMinimumSize(600, 400)
161         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
162         self.setCentralWidget(tabs)
163
164         g = self.config.get("winpos-qt",[100, 100, 840, 400])
165         self.setGeometry(g[0], g[1], g[2], g[3])
166
167         self.setWindowIcon(QIcon(":icons/electrum.png"))
168         self.init_menubar()
169
170         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
171         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
172         QShortcut(QKeySequence("Ctrl+R"), self, self.update_wallet)
173         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
174         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
175
176         for i in range(tabs.count()):
177             QShortcut(QKeySequence("Alt+" + str(i + 1)), self, lambda i=i: tabs.setCurrentIndex(i))
178
179         self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
180         self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.network.banner) )
181         self.connect(self, QtCore.SIGNAL('transaction_signal'), lambda: self.notify_transactions() )
182         self.connect(self, QtCore.SIGNAL('send_tx2'), self.send_tx2)
183         self.connect(self, QtCore.SIGNAL('send_tx3'), self.send_tx3)
184
185         self.history_list.setFocus(True)
186
187         # network callbacks
188         if self.network:
189             self.network.register_callback('updated', lambda: self.need_update.set())
190             self.network.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
191             self.network.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
192             self.network.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
193             self.network.register_callback('new_transaction', lambda: self.emit(QtCore.SIGNAL('transaction_signal')))
194
195             # set initial message
196             self.console.showMessage(self.network.banner)
197
198         self.wallet = None
199         self.init_lite()
200
201
202     def go_full(self):
203         self.config.set_key('lite_mode', False, True)
204         self.mini.hide()
205         self.show()
206         self.raise_()
207
208     def go_lite(self):
209         self.config.set_key('lite_mode', True, True)
210         self.hide()
211         self.mini.show()
212         self.mini.raise_()
213
214
215     def init_lite(self):
216         import lite_window
217         if not self.check_qt_version():
218             if self.config.get('lite_mode') is True:
219                 msg = "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI"
220                 QMessageBox.warning(None, "Could not start Lite GUI.", msg)
221                 self.config.set_key('lite_mode', False, True)
222                 sys.exit(0)
223             self.mini = None
224             self.show()
225             self.raise_()
226             return
227
228         actuator = lite_window.MiniActuator(self)
229
230         actuator.load_theme()
231
232         self.mini = lite_window.MiniWindow(actuator, self.go_full, self.config)
233
234         driver = lite_window.MiniDriver(self, self.mini)
235
236         if self.config.get('lite_mode') is True:
237             self.go_lite()
238         else:
239             self.go_full()
240
241
242     def check_qt_version(self):
243         qtVersion = qVersion()
244         return int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7
245
246
247     def update_account_selector(self):
248         # account selector
249         accounts = self.wallet.get_account_names()
250         self.account_selector.clear()
251         if len(accounts) > 1:
252             self.account_selector.addItems([_("All accounts")] + accounts.values())
253             self.account_selector.setCurrentIndex(0)
254             self.account_selector.show()
255         else:
256             self.account_selector.hide()
257
258
259     def load_wallet(self, wallet):
260         import electrum
261         self.wallet = wallet
262         self.accounts_expanded = self.wallet.storage.get('accounts_expanded',{})
263         self.current_account = self.wallet.storage.get("current_account", None)
264
265         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.wallet.storage.path
266         if self.wallet.is_watching_only(): title += ' [%s]' % (_('watching only'))
267         self.setWindowTitle( title )
268         self.update_wallet()
269         # Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
270         self.notify_transactions()
271         self.update_account_selector()
272         self.new_account.setEnabled(self.wallet.seed_version>4)
273         self.update_lock_icon()
274         self.update_buttons_on_seed()
275         self.update_console()
276
277         run_hook('load_wallet', wallet)
278
279
280     def open_wallet(self):
281         wallet_folder = self.wallet.storage.path
282         filename = unicode( QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) )
283         if not filename:
284             return
285
286         storage = WalletStorage({'wallet_path': filename})
287         if not storage.file_exists:
288             self.show_message("file not found "+ filename)
289             return
290
291         self.wallet.stop_threads()
292
293         # create new wallet
294         wallet = Wallet(storage)
295         wallet.start_threads(self.network)
296
297         self.load_wallet(wallet)
298
299
300
301     def backup_wallet(self):
302         import shutil
303         path = self.wallet.storage.path
304         wallet_folder = os.path.dirname(path)
305         filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a filename for the copy of your wallet'), wallet_folder) )
306         if not filename:
307             return
308
309         new_path = os.path.join(wallet_folder, filename)
310         if new_path != path:
311             try:
312                 shutil.copy2(path, new_path)
313                 QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
314             except (IOError, os.error), reason:
315                 QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
316
317
318     def new_wallet(self):
319         import installwizard
320
321         wallet_folder = os.path.dirname(self.wallet.storage.path)
322         filename = unicode( QFileDialog.getSaveFileName(self, _('Enter a new file name'), wallet_folder) )
323         if not filename:
324             return
325         filename = os.path.join(wallet_folder, filename)
326
327         storage = WalletStorage({'wallet_path': filename})
328         if storage.file_exists:
329             QMessageBox.critical(None, "Error", _("File exists"))
330             return
331
332         wizard = installwizard.InstallWizard(self.config, self.network, storage)
333         wallet = wizard.run()
334         if wallet:
335             self.load_wallet(wallet)
336
337
338
339     def init_menubar(self):
340         menubar = QMenuBar()
341
342         file_menu = menubar.addMenu(_("&File"))
343         file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
344         file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
345         file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
346         file_menu.addAction(_("&Quit"), self.close)
347
348         wallet_menu = menubar.addMenu(_("&Wallet"))
349         wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
350         self.new_account = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
351
352         wallet_menu.addSeparator()
353
354         wallet_menu.addAction(_("&Password"), self.change_password_dialog)
355         wallet_menu.addAction(_("&Seed"), self.show_seed_dialog)
356         wallet_menu.addAction(_("&Master Public Key"), self.show_master_public_key)
357
358         wallet_menu.addSeparator()
359         labels_menu = wallet_menu.addMenu(_("&Labels"))
360         labels_menu.addAction(_("&Import"), self.do_import_labels)
361         labels_menu.addAction(_("&Export"), self.do_export_labels)
362
363         keys_menu = wallet_menu.addMenu(_("&Private keys"))
364         keys_menu.addAction(_("&Import"), self.do_import_privkey)
365         keys_menu.addAction(_("&Export"), self.do_export_privkeys)
366
367         wallet_menu.addAction(_("&Export History"), self.do_export_history)
368
369         tools_menu = menubar.addMenu(_("&Tools"))
370
371         # Settings / Preferences are all reserved keywords in OSX using this as work around
372         tools_menu.addAction(_("Electrum preferences") if sys.platform == 'darwin' else _("Preferences"), self.settings_dialog)
373         tools_menu.addAction(_("&Network"), self.run_network_dialog)
374         tools_menu.addAction(_("&Plugins"), self.plugins_dialog)
375         tools_menu.addSeparator()
376         tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
377         #tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
378         tools_menu.addSeparator()
379
380         csv_transaction_menu = tools_menu.addMenu(_("&Create transaction"))
381         csv_transaction_menu.addAction(_("&From CSV file"), self.do_process_from_csv_file)
382         csv_transaction_menu.addAction(_("&From CSV text"), self.do_process_from_csv_text)
383
384         raw_transaction_menu = tools_menu.addMenu(_("&Load transaction"))
385         raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file)
386         raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text)
387         raw_transaction_menu.addAction(_("&From the blockchain"), self.do_process_from_txid)
388
389         help_menu = menubar.addMenu(_("&Help"))
390         help_menu.addAction(_("&About"), self.show_about)
391         help_menu.addAction(_("&Official website"), lambda: webbrowser.open("http://electrum.org"))
392         help_menu.addSeparator()
393         help_menu.addAction(_("&Documentation"), lambda: webbrowser.open("http://electrum.org/documentation.html")).setShortcut(QKeySequence.HelpContents)
394         help_menu.addAction(_("&Report Bug"), self.show_report_bug)
395
396         self.setMenuBar(menubar)
397
398     def show_about(self):
399         QMessageBox.about(self, "Electrum",
400             _("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."))
401
402     def show_report_bug(self):
403         QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
404             _("Please report any bugs as issues on github:")+" <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>")
405
406
407     def notify_transactions(self):
408         if not self.network or not self.network.is_connected():
409             return
410
411         print_error("Notifying GUI")
412         if len(self.network.interface.pending_transactions_for_notifications) > 0:
413             # Combine the transactions if there are more then three
414             tx_amount = len(self.network.interface.pending_transactions_for_notifications)
415             if(tx_amount >= 3):
416                 total_amount = 0
417                 for tx in self.network.interface.pending_transactions_for_notifications:
418                     is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
419                     if(v > 0):
420                         total_amount += v
421
422                 self.notify(_("%(txs)s new transactions received. Total amount received in the new transactions %(amount)s %(unit)s") \
423                                 % { 'txs' : tx_amount, 'amount' : self.format_amount(total_amount), 'unit' : self.base_unit()})
424
425                 self.network.interface.pending_transactions_for_notifications = []
426             else:
427               for tx in self.network.interface.pending_transactions_for_notifications:
428                   if tx:
429                       self.network.interface.pending_transactions_for_notifications.remove(tx)
430                       is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
431                       if(v > 0):
432                           self.notify(_("New transaction received. %(amount)s %(unit)s") % { 'amount' : self.format_amount(v), 'unit' : self.base_unit()})
433
434     def notify(self, message):
435         self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000)
436
437
438
439     # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
440     def getOpenFileName(self, title, filter = ""):
441         directory = self.config.get('io_dir', os.path.expanduser('~'))
442         fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
443         if fileName and directory != os.path.dirname(fileName):
444             self.config.set_key('io_dir', os.path.dirname(fileName), True)
445         return fileName
446
447     def getSaveFileName(self, title, filename, filter = ""):
448         directory = self.config.get('io_dir', os.path.expanduser('~'))
449         path = os.path.join( directory, filename )
450         fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
451         if fileName and directory != os.path.dirname(fileName):
452             self.config.set_key('io_dir', os.path.dirname(fileName), True)
453         return fileName
454
455     def close(self):
456         QMainWindow.close(self)
457         run_hook('close_main_window')
458
459     def connect_slots(self, sender):
460         self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
461         self.previous_payto_e=''
462
463     def timer_actions(self):
464         if self.need_update.is_set():
465             self.update_wallet()
466             self.need_update.clear()
467         run_hook('timer_actions')
468
469     def format_amount(self, x, is_diff=False, whitespaces=False):
470         return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
471
472     def read_amount(self, x):
473         if x in['.', '']: return None
474         p = pow(10, self.decimal_point)
475         return int( p * Decimal(x) )
476
477     def base_unit(self):
478         assert self.decimal_point in [5,8]
479         return "BTC" if self.decimal_point == 8 else "mBTC"
480
481
482     def update_status(self):
483         if self.network is None or not self.network.is_running():
484             text = _("Offline")
485             icon = QIcon(":icons/status_disconnected.png")
486
487         elif self.network.is_connected():
488             if not self.wallet.up_to_date:
489                 text = _("Synchronizing...")
490                 icon = QIcon(":icons/status_waiting.png")
491             elif self.network.server_lag > 1:
492                 text = _("Server is lagging (%d blocks)"%self.network.server_lag)
493                 icon = QIcon(":icons/status_lagging.png")
494             else:
495                 c, u = self.wallet.get_account_balance(self.current_account)
496                 text =  _( "Balance" ) + ": %s "%( self.format_amount(c) ) + self.base_unit()
497                 if u: text +=  " [%s unconfirmed]"%( self.format_amount(u,True).strip() )
498
499                 # append fiat balance and price from exchange rate plugin
500                 r = {}
501                 run_hook('get_fiat_status_text', c+u, r)
502                 quote = r.get(0)
503                 if quote:
504                     text += "%s"%quote
505
506                 self.tray.setToolTip(text)
507                 icon = QIcon(":icons/status_connected.png")
508         else:
509             text = _("Not connected")
510             icon = QIcon(":icons/status_disconnected.png")
511
512         self.balance_label.setText(text)
513         self.status_button.setIcon( icon )
514
515
516     def update_wallet(self):
517         self.update_status()
518         if self.wallet.up_to_date or not self.network or not self.network.is_connected():
519             self.update_history_tab()
520             self.update_receive_tab()
521             self.update_contacts_tab()
522             self.update_completions()
523
524
525     def create_history_tab(self):
526         self.history_list = l = MyTreeWidget(self)
527         l.setColumnCount(5)
528         for i,width in enumerate(self.column_widths['history']):
529             l.setColumnWidth(i, width)
530         l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
531         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
532         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
533
534         l.customContextMenuRequested.connect(self.create_history_menu)
535         return l
536
537
538     def create_history_menu(self, position):
539         self.history_list.selectedIndexes()
540         item = self.history_list.currentItem()
541         if not item: return
542         tx_hash = str(item.data(0, Qt.UserRole).toString())
543         if not tx_hash: return
544         menu = QMenu()
545         menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
546         menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
547         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
548         menu.addAction(_("View on Blockchain.info"), lambda: webbrowser.open("https://blockchain.info/tx/" + tx_hash))
549         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
550
551
552     def show_transaction(self, tx):
553         import transaction_dialog
554         d = transaction_dialog.TxDialog(tx, self)
555         d.exec_()
556
557     def tx_label_clicked(self, item, column):
558         if column==2 and item.isSelected():
559             self.is_edit=True
560             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
561             self.history_list.editItem( item, column )
562             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
563             self.is_edit=False
564
565     def tx_label_changed(self, item, column):
566         if self.is_edit:
567             return
568         self.is_edit=True
569         tx_hash = str(item.data(0, Qt.UserRole).toString())
570         tx = self.wallet.transactions.get(tx_hash)
571         text = unicode( item.text(2) )
572         self.wallet.set_label(tx_hash, text)
573         if text:
574             item.setForeground(2, QBrush(QColor('black')))
575         else:
576             text = self.wallet.get_default_label(tx_hash)
577             item.setText(2, text)
578             item.setForeground(2, QBrush(QColor('gray')))
579         self.is_edit=False
580
581
582     def edit_label(self, is_recv):
583         l = self.receive_list if is_recv else self.contacts_list
584         item = l.currentItem()
585         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
586         l.editItem( item, 1 )
587         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
588
589
590
591     def address_label_clicked(self, item, column, l, column_addr, column_label):
592         if column == column_label and item.isSelected():
593             is_editable = item.data(0, 32).toBool()
594             if not is_editable:
595                 return
596             addr = unicode( item.text(column_addr) )
597             label = unicode( item.text(column_label) )
598             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
599             l.editItem( item, column )
600             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
601
602
603     def address_label_changed(self, item, column, l, column_addr, column_label):
604         if column == column_label:
605             addr = unicode( item.text(column_addr) )
606             text = unicode( item.text(column_label) )
607             is_editable = item.data(0, 32).toBool()
608             if not is_editable:
609                 return
610
611             changed = self.wallet.set_label(addr, text)
612             if changed:
613                 self.update_history_tab()
614                 self.update_completions()
615
616             self.current_item_changed(item)
617
618         run_hook('item_changed', item, column)
619
620
621     def current_item_changed(self, a):
622         run_hook('current_item_changed', a)
623
624
625
626     def update_history_tab(self):
627
628         self.history_list.clear()
629         for item in self.wallet.get_tx_history(self.current_account):
630             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
631             time_str = _("unknown")
632             if conf > 0:
633                 try:
634                     time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
635                 except Exception:
636                     time_str = _("error")
637
638             if conf == -1:
639                 time_str = 'unverified'
640                 icon = QIcon(":icons/unconfirmed.png")
641             elif conf == 0:
642                 time_str = 'pending'
643                 icon = QIcon(":icons/unconfirmed.png")
644             elif conf < 6:
645                 icon = QIcon(":icons/clock%d.png"%conf)
646             else:
647                 icon = QIcon(":icons/confirmed.png")
648
649             if value is not None:
650                 v_str = self.format_amount(value, True, whitespaces=True)
651             else:
652                 v_str = '--'
653
654             balance_str = self.format_amount(balance, whitespaces=True)
655
656             if tx_hash:
657                 label, is_default_label = self.wallet.get_label(tx_hash)
658             else:
659                 label = _('Pruned transaction outputs')
660                 is_default_label = False
661
662             item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
663             item.setFont(2, QFont(MONOSPACE_FONT))
664             item.setFont(3, QFont(MONOSPACE_FONT))
665             item.setFont(4, QFont(MONOSPACE_FONT))
666             if value < 0:
667                 item.setForeground(3, QBrush(QColor("#BC1E1E")))
668             if tx_hash:
669                 item.setData(0, Qt.UserRole, tx_hash)
670                 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
671             if is_default_label:
672                 item.setForeground(2, QBrush(QColor('grey')))
673
674             item.setIcon(0, icon)
675             self.history_list.insertTopLevelItem(0,item)
676
677
678         self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
679         run_hook('history_tab_update')
680
681
682     def create_send_tab(self):
683         w = QWidget()
684
685         grid = QGridLayout()
686         grid.setSpacing(8)
687         grid.setColumnMinimumWidth(3,300)
688         grid.setColumnStretch(5,1)
689
690
691         self.payto_e = QLineEdit()
692         grid.addWidget(QLabel(_('Pay to')), 1, 0)
693         grid.addWidget(self.payto_e, 1, 1, 1, 3)
694
695         grid.addWidget(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)')), 1, 4)
696
697         completer = QCompleter()
698         completer.setCaseSensitivity(False)
699         self.payto_e.setCompleter(completer)
700         completer.setModel(self.completions)
701
702         self.message_e = QLineEdit()
703         grid.addWidget(QLabel(_('Description')), 2, 0)
704         grid.addWidget(self.message_e, 2, 1, 1, 3)
705         grid.addWidget(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.')), 2, 4)
706
707         self.from_label = QLabel(_('From'))
708         grid.addWidget(self.from_label, 3, 0)
709         self.from_list = QTreeWidget(self)
710         self.from_list.setColumnCount(2)
711         self.from_list.setColumnWidth(0, 350)
712         self.from_list.setColumnWidth(1, 50)
713         self.from_list.setHeaderHidden (True)
714         self.from_list.setMaximumHeight(80)
715         grid.addWidget(self.from_list, 3, 1, 1, 3)
716         self.set_pay_from([])
717
718         self.amount_e = AmountEdit(self.base_unit)
719         grid.addWidget(QLabel(_('Amount')), 4, 0)
720         grid.addWidget(self.amount_e, 4, 1, 1, 2)
721         grid.addWidget(HelpButton(
722                 _('Amount to be sent.') + '\n\n' \
723                     + _('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.') \
724                     + '\n\n' + _('Keyboard shortcut: type "!" to send all your coins.')), 4, 3)
725
726         self.fee_e = AmountEdit(self.base_unit)
727         grid.addWidget(QLabel(_('Fee')), 5, 0)
728         grid.addWidget(self.fee_e, 5, 1, 1, 2)
729         grid.addWidget(HelpButton(
730                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
731                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
732                     + _('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)
733
734         run_hook('exchange_rate_button', grid)
735
736         self.send_button = EnterButton(_("Send"), self.do_send)
737         grid.addWidget(self.send_button, 6, 1)
738
739         b = EnterButton(_("Clear"),self.do_clear)
740         grid.addWidget(b, 6, 2)
741
742         self.payto_sig = QLabel('')
743         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
744
745         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
746         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
747         w.setLayout(grid)
748
749         w2 = QWidget()
750         vbox = QVBoxLayout()
751         vbox.addWidget(w)
752         vbox.addStretch(1)
753         w2.setLayout(vbox)
754
755         def entry_changed( is_fee ):
756             self.funds_error = False
757
758             if self.amount_e.is_shortcut:
759                 self.amount_e.is_shortcut = False
760                 sendable = self.get_sendable_balance()
761                 # there is only one output because we are completely spending inputs
762                 inputs, total, fee = self.wallet.choose_tx_inputs( sendable, 0, 1, self.get_payment_sources())
763                 fee = self.wallet.estimated_fee(inputs, 1)
764                 amount = total - fee
765                 self.amount_e.setText( self.format_amount(amount) )
766                 self.fee_e.setText( self.format_amount( fee ) )
767                 return
768
769             amount = self.read_amount(str(self.amount_e.text()))
770             fee = self.read_amount(str(self.fee_e.text()))
771
772             if not is_fee: fee = None
773             if amount is None:
774                 return
775             # assume that there will be 2 outputs (one for change)
776             inputs, total, fee = self.wallet.choose_tx_inputs(amount, fee, 2, self.get_payment_sources())
777             if not is_fee:
778                 self.fee_e.setText( self.format_amount( fee ) )
779             if inputs:
780                 palette = QPalette()
781                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
782                 text = ""
783             else:
784                 palette = QPalette()
785                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
786                 self.funds_error = True
787                 text = _( "Not enough funds" )
788                 c, u = self.wallet.get_frozen_balance()
789                 if c+u: text += ' (' + self.format_amount(c+u).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
790
791             self.statusBar().showMessage(text)
792             self.amount_e.setPalette(palette)
793             self.fee_e.setPalette(palette)
794
795         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
796         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
797
798         run_hook('create_send_tab', grid)
799         return w2
800
801
802     def set_pay_from(self, l):
803         self.pay_from = l
804         self.from_list.clear()
805         self.from_label.setHidden(len(self.pay_from) == 0)
806         self.from_list.setHidden(len(self.pay_from) == 0)
807         for addr in self.pay_from:
808             c, u = self.wallet.get_addr_balance(addr)
809             balance = self.format_amount(c + u)
810             self.from_list.addTopLevelItem(QTreeWidgetItem( [addr, balance] ))
811
812
813     def update_completions(self):
814         l = []
815         for addr,label in self.wallet.labels.items():
816             if addr in self.wallet.addressbook:
817                 l.append( label + '  <' + addr + '>')
818
819         run_hook('update_completions', l)
820         self.completions.setStringList(l)
821
822
823     def protected(func):
824         return lambda s, *args: s.do_protect(func, args)
825
826
827     def do_send(self):
828
829         label = unicode( self.message_e.text() )
830         r = unicode( self.payto_e.text() )
831         r = r.strip()
832
833         # label or alias, with address in brackets
834         m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
835         to_address = m.group(2) if m else r
836
837         if not is_valid(to_address):
838             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
839             return
840
841         try:
842             amount = self.read_amount(unicode( self.amount_e.text()))
843         except Exception:
844             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
845             return
846         try:
847             fee = self.read_amount(unicode( self.fee_e.text()))
848         except Exception:
849             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
850             return
851
852         confirm_amount = self.config.get('confirm_amount', 100000000)
853         if amount >= confirm_amount:
854             if not self.question(_("send %(amount)s to %(address)s?")%{ 'amount' : self.format_amount(amount) + ' '+ self.base_unit(), 'address' : to_address}):
855                 return
856             
857         confirm_fee = self.config.get('confirm_fee', 100000)
858         if fee >= confirm_fee:
859             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()}):
860                 return
861
862         self.send_tx(to_address, amount, fee, label)
863
864
865     def waiting_dialog(self, message):
866         d = QDialog(self)
867         d.setWindowTitle('Please wait')
868         l = QLabel(message)
869         vbox = QVBoxLayout(d)
870         vbox.addWidget(l)
871         d.show()
872         return d
873
874
875     @protected
876     def send_tx(self, to_address, amount, fee, label, password):
877
878         # first, create an unsigned tx 
879         domain = self.get_payment_sources()
880         outputs = [(to_address, amount)]
881         try:
882             tx = self.wallet.make_unsigned_transaction(outputs, fee, None, domain)
883             tx.error = None
884         except Exception as e:
885             traceback.print_exc(file=sys.stdout)
886             self.show_message(str(e))
887             return
888
889         # call hook to see if plugin needs gui interaction
890         run_hook('send_tx', tx)
891
892         # sign the tx
893         def sign_thread():
894             time.sleep(0.1)
895             keypairs = {}
896             self.wallet.add_keypairs_from_wallet(tx, keypairs, password)
897             self.wallet.sign_transaction(tx, keypairs, password)
898             self.signed_tx_data = (tx, fee, label)
899             self.emit(SIGNAL('send_tx2'))
900         self.tx_wait_dialog = self.waiting_dialog('Signing..')
901         threading.Thread(target=sign_thread).start()
902
903         # add recipient to addressbook
904         if to_address not in self.wallet.addressbook and not self.wallet.is_mine(to_address):
905             self.wallet.addressbook.append(to_address)
906
907
908     def send_tx2(self):
909         tx, fee, label = self.signed_tx_data
910         self.tx_wait_dialog.accept()
911         
912         if tx.error:
913             self.show_message(tx.error)
914             return
915
916         if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
917             QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
918             return
919
920         if label:
921             self.wallet.set_label(tx.hash(), label)
922
923         if not tx.is_complete():
924             self.show_transaction(tx)
925             return
926
927         # broadcast the tx
928         def broadcast_thread():
929             self.tx_broadcast_result =  self.wallet.sendtx(tx)
930             self.emit(SIGNAL('send_tx3'))
931         self.tx_broadcast_dialog = self.waiting_dialog('Broadcasting..')
932         threading.Thread(target=broadcast_thread).start()
933
934
935     def send_tx3(self):
936         self.tx_broadcast_dialog.accept()
937         status, msg = self.tx_broadcast_result
938         if status:
939             QMessageBox.information(self, '', _('Payment sent.') + '\n' + msg, _('OK'))
940             self.do_clear()
941             self.update_contacts_tab()
942         else:
943             QMessageBox.warning(self, _('Error'), msg, _('OK'))
944
945
946
947
948
949
950     def set_url(self, url):
951         address, amount, label, message, signature, identity, url = util.parse_url(url)
952
953         try:
954             if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
955             elif amount: amount = str(Decimal(amount))
956         except Exception:
957             amount = "0.0"
958             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
959
960         if self.mini:
961             self.mini.set_payment_fields(address, amount)
962
963         if label and self.wallet.labels.get(address) != label:
964             if self.question('Give label "%s" to address %s ?'%(label,address)):
965                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
966                     self.wallet.addressbook.append(address)
967                 self.wallet.set_label(address, label)
968
969         run_hook('set_url', url, self.show_message, self.question)
970
971         self.tabs.setCurrentIndex(1)
972         label = self.wallet.labels.get(address)
973         m_addr = label + '  <'+ address +'>' if label else address
974         self.payto_e.setText(m_addr)
975
976         self.message_e.setText(message)
977         if amount:
978             self.amount_e.setText(amount)
979
980         if identity:
981             self.set_frozen(self.payto_e,True)
982             self.set_frozen(self.amount_e,True)
983             self.set_frozen(self.message_e,True)
984             self.payto_sig.setText( '      '+_('The bitcoin URI was signed by')+' ' + identity )
985         else:
986             self.payto_sig.setVisible(False)
987
988     def do_clear(self):
989         self.payto_sig.setVisible(False)
990         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
991             e.setText('')
992             self.set_frozen(e,False)
993
994         self.set_pay_from([])
995         self.update_status()
996
997     def set_frozen(self,entry,frozen):
998         if frozen:
999             entry.setReadOnly(True)
1000             entry.setFrame(False)
1001             palette = QPalette()
1002             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
1003             entry.setPalette(palette)
1004         else:
1005             entry.setReadOnly(False)
1006             entry.setFrame(True)
1007             palette = QPalette()
1008             palette.setColor(entry.backgroundRole(), QColor('white'))
1009             entry.setPalette(palette)
1010
1011
1012     def set_addrs_frozen(self,addrs,freeze):
1013         for addr in addrs:
1014             if not addr: continue
1015             if addr in self.wallet.frozen_addresses and not freeze:
1016                 self.wallet.unfreeze(addr)
1017             elif addr not in self.wallet.frozen_addresses and freeze:
1018                 self.wallet.freeze(addr)
1019         self.update_receive_tab()
1020
1021
1022
1023     def create_list_tab(self, headers):
1024         "generic tab creation method"
1025         l = MyTreeWidget(self)
1026         l.setColumnCount( len(headers) )
1027         l.setHeaderLabels( headers )
1028
1029         w = QWidget()
1030         vbox = QVBoxLayout()
1031         w.setLayout(vbox)
1032
1033         vbox.setMargin(0)
1034         vbox.setSpacing(0)
1035         vbox.addWidget(l)
1036         buttons = QWidget()
1037         vbox.addWidget(buttons)
1038
1039         hbox = QHBoxLayout()
1040         hbox.setMargin(0)
1041         hbox.setSpacing(0)
1042         buttons.setLayout(hbox)
1043
1044         return l,w,hbox
1045
1046
1047     def create_receive_tab(self):
1048         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1049         l.setContextMenuPolicy(Qt.CustomContextMenu)
1050         l.customContextMenuRequested.connect(self.create_receive_menu)
1051         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1052         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1053         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1054         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1055         self.receive_list = l
1056         self.receive_buttons_hbox = hbox
1057         hbox.addStretch(1)
1058         return w
1059
1060
1061
1062
1063     def save_column_widths(self):
1064         self.column_widths["receive"] = []
1065         for i in range(self.receive_list.columnCount() -1):
1066             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1067
1068         self.column_widths["history"] = []
1069         for i in range(self.history_list.columnCount() - 1):
1070             self.column_widths["history"].append(self.history_list.columnWidth(i))
1071
1072         self.column_widths["contacts"] = []
1073         for i in range(self.contacts_list.columnCount() - 1):
1074             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1075
1076         self.config.set_key("column_widths_2", self.column_widths, True)
1077
1078
1079     def create_contacts_tab(self):
1080         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1081         l.setContextMenuPolicy(Qt.CustomContextMenu)
1082         l.customContextMenuRequested.connect(self.create_contact_menu)
1083         for i,width in enumerate(self.column_widths['contacts']):
1084             l.setColumnWidth(i, width)
1085
1086         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1087         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1088         self.contacts_list = l
1089         self.contacts_buttons_hbox = hbox
1090         hbox.addStretch(1)
1091         return w
1092
1093
1094     def delete_imported_key(self, addr):
1095         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1096             self.wallet.delete_imported_key(addr)
1097             self.update_receive_tab()
1098             self.update_history_tab()
1099
1100     def edit_account_label(self, k):
1101         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1102         if ok:
1103             label = unicode(text)
1104             self.wallet.set_label(k,label)
1105             self.update_receive_tab()
1106
1107     def account_set_expanded(self, item, k, b):
1108         item.setExpanded(b)
1109         self.accounts_expanded[k] = b
1110
1111     def create_account_menu(self, position, k, item):
1112         menu = QMenu()
1113         if item.isExpanded():
1114             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1115         else:
1116             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1117         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1118         if self.wallet.seed_version > 4:
1119             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1120         if self.wallet.account_is_pending(k):
1121             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1122         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1123
1124     def delete_pending_account(self, k):
1125         self.wallet.delete_pending_account(k)
1126         self.update_receive_tab()
1127
1128     def create_receive_menu(self, position):
1129         # fixme: this function apparently has a side effect.
1130         # if it is not called the menu pops up several times
1131         #self.receive_list.selectedIndexes()
1132
1133         selected = self.receive_list.selectedItems()
1134         multi_select = len(selected) > 1
1135         addrs = [unicode(item.text(0)) for item in selected]
1136         if not multi_select:
1137             item = self.receive_list.itemAt(position)
1138             if not item: return
1139
1140             addr = addrs[0]
1141             if not is_valid(addr):
1142                 k = str(item.data(0,32).toString())
1143                 if k:
1144                     self.create_account_menu(position, k, item)
1145                 else:
1146                     item.setExpanded(not item.isExpanded())
1147                 return
1148
1149         menu = QMenu()
1150         if not multi_select:
1151             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1152             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1153             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1154             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1155             if self.wallet.seed:
1156                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1157                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1158                 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1159             if addr in self.wallet.imported_keys:
1160                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1161
1162         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1163             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1164         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1165             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1166
1167         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1168             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1169
1170         run_hook('receive_menu', menu, addrs)
1171         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1172
1173
1174     def get_sendable_balance(self):
1175         return sum(sum(self.wallet.get_addr_balance(a)) for a in self.get_payment_sources())
1176
1177
1178     def get_payment_sources(self):
1179         if self.pay_from:
1180             return self.pay_from
1181         else:
1182             return self.wallet.get_account_addresses(self.current_account)
1183
1184
1185     def send_from_addresses(self, addrs):
1186         self.set_pay_from( addrs )
1187         self.tabs.setCurrentIndex(1)
1188
1189
1190     def payto(self, addr):
1191         if not addr: return
1192         label = self.wallet.labels.get(addr)
1193         m_addr = label + '  <' + addr + '>' if label else addr
1194         self.tabs.setCurrentIndex(1)
1195         self.payto_e.setText(m_addr)
1196         self.amount_e.setFocus()
1197
1198
1199     def delete_contact(self, x):
1200         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1201             self.wallet.delete_contact(x)
1202             self.wallet.set_label(x, None)
1203             self.update_history_tab()
1204             self.update_contacts_tab()
1205             self.update_completions()
1206
1207
1208     def create_contact_menu(self, position):
1209         item = self.contacts_list.itemAt(position)
1210         menu = QMenu()
1211         if not item:
1212             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1213         else:
1214             addr = unicode(item.text(0))
1215             label = unicode(item.text(1))
1216             is_editable = item.data(0,32).toBool()
1217             payto_addr = item.data(0,33).toString()
1218             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1219             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1220             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1221             if is_editable:
1222                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1223                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1224
1225         run_hook('create_contact_menu', menu, item)
1226         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1227
1228
1229     def update_receive_item(self, item):
1230         item.setFont(0, QFont(MONOSPACE_FONT))
1231         address = str(item.data(0,0).toString())
1232         label = self.wallet.labels.get(address,'')
1233         item.setData(1,0,label)
1234         item.setData(0,32, True) # is editable
1235
1236         run_hook('update_receive_item', address, item)
1237
1238         if not self.wallet.is_mine(address): return
1239
1240         c, u = self.wallet.get_addr_balance(address)
1241         balance = self.format_amount(c + u)
1242         item.setData(2,0,balance)
1243
1244         if address in self.wallet.frozen_addresses:
1245             item.setBackgroundColor(0, QColor('lightblue'))
1246
1247
1248     def update_receive_tab(self):
1249         l = self.receive_list
1250
1251         l.clear()
1252         l.setColumnHidden(2, False)
1253         l.setColumnHidden(3, False)
1254         for i,width in enumerate(self.column_widths['receive']):
1255             l.setColumnWidth(i, width)
1256
1257         if self.current_account is None:
1258             account_items = self.wallet.accounts.items()
1259         elif self.current_account != -1:
1260             account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1261         else:
1262             account_items = []
1263
1264         for k, account in account_items:
1265             name = self.wallet.get_account_name(k)
1266             c,u = self.wallet.get_account_balance(k)
1267             account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1268             l.addTopLevelItem(account_item)
1269             account_item.setExpanded(self.accounts_expanded.get(k, True))
1270             account_item.setData(0, 32, k)
1271
1272             if not self.wallet.is_seeded(k):
1273                 icon = QIcon(":icons/key.png")
1274                 account_item.setIcon(0, icon)
1275
1276             for is_change in ([0,1]):
1277                 name = _("Receiving") if not is_change else _("Change")
1278                 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1279                 account_item.addChild(seq_item)
1280                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1281                 used_flag = False
1282                 if not is_change: seq_item.setExpanded(True)
1283
1284                 is_red = False
1285                 gap = 0
1286
1287                 for address in account.get_addresses(is_change):
1288                     h = self.wallet.history.get(address,[])
1289
1290                     if h == []:
1291                         gap += 1
1292                         if gap > self.wallet.gap_limit:
1293                             is_red = True
1294                     else:
1295                         gap = 0
1296
1297                     c, u = self.wallet.get_addr_balance(address)
1298                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1299                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1300                     self.update_receive_item(item)
1301                     if is_red:
1302                         item.setBackgroundColor(1, QColor('red'))
1303                     if len(h) > 0 and c == -u:
1304                         if not used_flag:
1305                             seq_item.insertChild(0,used_item)
1306                             used_flag = True
1307                         used_item.addChild(item)
1308                     else:
1309                         seq_item.addChild(item)
1310
1311
1312         for k, addr in self.wallet.get_pending_accounts():
1313             name = self.wallet.labels.get(k,'')
1314             account_item = QTreeWidgetItem( [ name + "  [ "+_('pending account')+" ]", '', '', ''] )
1315             self.update_receive_item(item)
1316             l.addTopLevelItem(account_item)
1317             account_item.setExpanded(True)
1318             account_item.setData(0, 32, k)
1319             item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1320             account_item.addChild(item)
1321             self.update_receive_item(item)
1322
1323
1324         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1325             c,u = self.wallet.get_imported_balance()
1326             account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1327             l.addTopLevelItem(account_item)
1328             account_item.setExpanded(True)
1329             for address in self.wallet.imported_keys.keys():
1330                 item = QTreeWidgetItem( [ address, '', '', ''] )
1331                 self.update_receive_item(item)
1332                 account_item.addChild(item)
1333
1334
1335         # we use column 1 because column 0 may be hidden
1336         l.setCurrentItem(l.topLevelItem(0),1)
1337
1338
1339     def update_contacts_tab(self):
1340         l = self.contacts_list
1341         l.clear()
1342
1343         for address in self.wallet.addressbook:
1344             label = self.wallet.labels.get(address,'')
1345             n = self.wallet.get_num_tx(address)
1346             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1347             item.setFont(0, QFont(MONOSPACE_FONT))
1348             # 32 = label can be edited (bool)
1349             item.setData(0,32, True)
1350             # 33 = payto string
1351             item.setData(0,33, address)
1352             l.addTopLevelItem(item)
1353
1354         run_hook('update_contacts_tab', l)
1355         l.setCurrentItem(l.topLevelItem(0))
1356
1357
1358
1359     def create_console_tab(self):
1360         from console import Console
1361         self.console = console = Console()
1362         return console
1363
1364
1365     def update_console(self):
1366         console = self.console
1367         console.history = self.config.get("console-history",[])
1368         console.history_index = len(console.history)
1369
1370         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1371         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1372
1373         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1374         methods = {}
1375         def mkfunc(f, method):
1376             return lambda *args: apply( f, (method, args, self.password_dialog ))
1377         for m in dir(c):
1378             if m[0]=='_' or m in ['network','wallet']: continue
1379             methods[m] = mkfunc(c._run, m)
1380
1381         console.updateNamespace(methods)
1382
1383
1384     def change_account(self,s):
1385         if s == _("All accounts"):
1386             self.current_account = None
1387         else:
1388             accounts = self.wallet.get_account_names()
1389             for k, v in accounts.items():
1390                 if v == s:
1391                     self.current_account = k
1392         self.update_history_tab()
1393         self.update_status()
1394         self.update_receive_tab()
1395
1396     def create_status_bar(self):
1397
1398         sb = QStatusBar()
1399         sb.setFixedHeight(35)
1400         qtVersion = qVersion()
1401
1402         self.balance_label = QLabel("")
1403         sb.addWidget(self.balance_label)
1404
1405         from version_getter import UpdateLabel
1406         self.updatelabel = UpdateLabel(self.config, sb)
1407
1408         self.account_selector = QComboBox()
1409         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1410         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1411         sb.addPermanentWidget(self.account_selector)
1412
1413         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1414             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1415
1416         self.lock_icon = QIcon()
1417         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1418         sb.addPermanentWidget( self.password_button )
1419
1420         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1421         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1422         sb.addPermanentWidget( self.seed_button )
1423         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1424         sb.addPermanentWidget( self.status_button )
1425
1426         run_hook('create_status_bar', (sb,))
1427
1428         self.setStatusBar(sb)
1429
1430
1431     def update_lock_icon(self):
1432         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1433         self.password_button.setIcon( icon )
1434
1435
1436     def update_buttons_on_seed(self):
1437         if not self.wallet.is_watching_only():
1438            self.seed_button.show()
1439            self.password_button.show()
1440            self.send_button.setText(_("Send"))
1441         else:
1442            self.password_button.hide()
1443            self.seed_button.hide()
1444            self.send_button.setText(_("Create unsigned transaction"))
1445
1446
1447     def change_password_dialog(self):
1448         from password_dialog import PasswordDialog
1449         d = PasswordDialog(self.wallet, self)
1450         d.run()
1451         self.update_lock_icon()
1452
1453
1454     def new_contact_dialog(self):
1455
1456         d = QDialog(self)
1457         d.setWindowTitle(_("New Contact"))
1458         vbox = QVBoxLayout(d)
1459         vbox.addWidget(QLabel(_('New Contact')+':'))
1460
1461         grid = QGridLayout()
1462         line1 = QLineEdit()
1463         line2 = QLineEdit()
1464         grid.addWidget(QLabel(_("Address")), 1, 0)
1465         grid.addWidget(line1, 1, 1)
1466         grid.addWidget(QLabel(_("Name")), 2, 0)
1467         grid.addWidget(line2, 2, 1)
1468
1469         vbox.addLayout(grid)
1470         vbox.addLayout(ok_cancel_buttons(d))
1471
1472         if not d.exec_():
1473             return
1474
1475         address = str(line1.text())
1476         label = unicode(line2.text())
1477
1478         if not is_valid(address):
1479             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1480             return
1481
1482         self.wallet.add_contact(address)
1483         if label:
1484             self.wallet.set_label(address, label)
1485
1486         self.update_contacts_tab()
1487         self.update_history_tab()
1488         self.update_completions()
1489         self.tabs.setCurrentIndex(3)
1490
1491
1492     @protected
1493     def new_account_dialog(self, password):
1494
1495         dialog = QDialog(self)
1496         dialog.setModal(1)
1497         dialog.setWindowTitle(_("New Account"))
1498
1499         vbox = QVBoxLayout()
1500         vbox.addWidget(QLabel(_('Account name')+':'))
1501         e = QLineEdit()
1502         vbox.addWidget(e)
1503         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1504             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1505         l = QLabel(msg)
1506         l.setWordWrap(True)
1507         vbox.addWidget(l)
1508
1509         vbox.addLayout(ok_cancel_buttons(dialog))
1510         dialog.setLayout(vbox)
1511         r = dialog.exec_()
1512         if not r: return
1513
1514         name = str(e.text())
1515         if not name: return
1516
1517         self.wallet.create_pending_account('1of1', name, password)
1518         self.update_receive_tab()
1519         self.tabs.setCurrentIndex(2)
1520
1521
1522
1523     def show_master_public_key_old(self):
1524         dialog = QDialog(self)
1525         dialog.setModal(1)
1526         dialog.setWindowTitle(_("Master Public Key"))
1527
1528         main_text = QTextEdit()
1529         main_text.setText(self.wallet.get_master_public_key())
1530         main_text.setReadOnly(True)
1531         main_text.setMaximumHeight(170)
1532         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1533
1534         ok_button = QPushButton(_("OK"))
1535         ok_button.setDefault(True)
1536         ok_button.clicked.connect(dialog.accept)
1537
1538         main_layout = QGridLayout()
1539         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1540
1541         main_layout.addWidget(main_text, 1, 0)
1542         main_layout.addWidget(qrw, 1, 1 )
1543
1544         vbox = QVBoxLayout()
1545         vbox.addLayout(main_layout)
1546         vbox.addLayout(close_button(dialog))
1547         dialog.setLayout(vbox)
1548         dialog.exec_()
1549
1550
1551     def show_master_public_key(self):
1552
1553         if self.wallet.seed_version == 4:
1554             self.show_master_public_key_old()
1555             return
1556
1557         dialog = QDialog(self)
1558         dialog.setModal(1)
1559         dialog.setWindowTitle(_("Master Public Keys"))
1560
1561         mpk_text = QTextEdit()
1562         mpk_text.setReadOnly(True)
1563         mpk_text.setMaximumHeight(170)
1564         mpk_qrw = QRCodeWidget()
1565
1566         main_layout = QGridLayout()
1567
1568         main_layout.addWidget(QLabel(_('Key')), 1, 0)
1569         main_layout.addWidget(mpk_text, 1, 1)
1570         main_layout.addWidget(mpk_qrw, 1, 2)
1571
1572         def update(key):
1573             xpub = self.wallet.master_public_keys[str(key)]
1574             mpk_text.setText(xpub)
1575             mpk_qrw.set_addr(xpub)
1576             mpk_qrw.update_qr()
1577
1578         key_selector = QComboBox()
1579         keys = sorted(self.wallet.master_public_keys.keys())
1580         key_selector.addItems(keys)
1581
1582         main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1583         main_layout.addWidget(key_selector, 0, 1)
1584         dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1585
1586         update(keys[0])
1587
1588         vbox = QVBoxLayout()
1589         vbox.addLayout(main_layout)
1590         vbox.addLayout(close_button(dialog))
1591
1592         dialog.setLayout(vbox)
1593         dialog.exec_()
1594
1595
1596     @protected
1597     def show_seed_dialog(self, password):
1598         if self.wallet.is_watching_only():
1599             QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1600             return
1601
1602         if self.wallet.seed:
1603             try:
1604                 mnemonic = self.wallet.get_mnemonic(password)
1605             except Exception:
1606                 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1607                 return
1608             from seed_dialog import SeedDialog
1609             d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1610             d.exec_()
1611         else:
1612             l = {}
1613             for k in self.wallet.master_private_keys.keys():
1614                 pk = self.wallet.get_master_private_key(k, password)
1615                 l[k] = pk
1616             from seed_dialog import PrivateKeysDialog
1617             d = PrivateKeysDialog(self,l)
1618             d.exec_()
1619
1620
1621
1622
1623
1624     def show_qrcode(self, data, title = _("QR code")):
1625         if not data: return
1626         d = QDialog(self)
1627         d.setModal(1)
1628         d.setWindowTitle(title)
1629         d.setMinimumSize(270, 300)
1630         vbox = QVBoxLayout()
1631         qrw = QRCodeWidget(data)
1632         vbox.addWidget(qrw, 1)
1633         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1634         hbox = QHBoxLayout()
1635         hbox.addStretch(1)
1636
1637         filename = os.path.join(self.config.path, "qrcode.bmp")
1638
1639         def print_qr():
1640             bmp.save_qrcode(qrw.qr, filename)
1641             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1642
1643         def copy_to_clipboard():
1644             bmp.save_qrcode(qrw.qr, filename)
1645             self.app.clipboard().setImage(QImage(filename))
1646             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1647
1648         b = QPushButton(_("Copy"))
1649         hbox.addWidget(b)
1650         b.clicked.connect(copy_to_clipboard)
1651
1652         b = QPushButton(_("Save"))
1653         hbox.addWidget(b)
1654         b.clicked.connect(print_qr)
1655
1656         b = QPushButton(_("Close"))
1657         hbox.addWidget(b)
1658         b.clicked.connect(d.accept)
1659         b.setDefault(True)
1660
1661         vbox.addLayout(hbox)
1662         d.setLayout(vbox)
1663         d.exec_()
1664
1665
1666     def do_protect(self, func, args):
1667         if self.wallet.use_encryption:
1668             password = self.password_dialog()
1669             if not password:
1670                 return
1671         else:
1672             password = None
1673
1674         if args != (False,):
1675             args = (self,) + args + (password,)
1676         else:
1677             args = (self,password)
1678         apply( func, args)
1679
1680
1681     def show_public_keys(self, address):
1682         if not address: return
1683         try:
1684             pubkey_list = self.wallet.get_public_keys(address)
1685         except Exception as e:
1686             traceback.print_exc(file=sys.stdout)
1687             self.show_message(str(e))
1688             return
1689
1690         d = QDialog(self)
1691         d.setMinimumSize(600, 200)
1692         d.setModal(1)
1693         vbox = QVBoxLayout()
1694         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1695         vbox.addWidget( QLabel(_("Public key") + ':'))
1696         keys = QTextEdit()
1697         keys.setReadOnly(True)
1698         keys.setText('\n'.join(pubkey_list))
1699         vbox.addWidget(keys)
1700         #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1701         vbox.addLayout(close_button(d))
1702         d.setLayout(vbox)
1703         d.exec_()
1704
1705     @protected
1706     def show_private_key(self, address, password):
1707         if not address: return
1708         try:
1709             pk_list = self.wallet.get_private_key(address, password)
1710         except Exception as e:
1711             traceback.print_exc(file=sys.stdout)
1712             self.show_message(str(e))
1713             return
1714
1715         d = QDialog(self)
1716         d.setMinimumSize(600, 200)
1717         d.setModal(1)
1718         vbox = QVBoxLayout()
1719         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1720         vbox.addWidget( QLabel(_("Private key") + ':'))
1721         keys = QTextEdit()
1722         keys.setReadOnly(True)
1723         keys.setText('\n'.join(pk_list))
1724         vbox.addWidget(keys)
1725         vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1726         vbox.addLayout(close_button(d))
1727         d.setLayout(vbox)
1728         d.exec_()
1729
1730
1731     @protected
1732     def do_sign(self, address, message, signature, password):
1733         message = unicode(message.toPlainText())
1734         message = message.encode('utf-8')
1735         try:
1736             sig = self.wallet.sign_message(str(address.text()), message, password)
1737             signature.setText(sig)
1738         except Exception as e:
1739             self.show_message(str(e))
1740
1741     def do_verify(self, address, message, signature):
1742         message = unicode(message.toPlainText())
1743         message = message.encode('utf-8')
1744         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1745             self.show_message(_("Signature verified"))
1746         else:
1747             self.show_message(_("Error: wrong signature"))
1748
1749
1750     def sign_verify_message(self, address=''):
1751         d = QDialog(self)
1752         d.setModal(1)
1753         d.setWindowTitle(_('Sign/verify Message'))
1754         d.setMinimumSize(410, 290)
1755
1756         layout = QGridLayout(d)
1757
1758         message_e = QTextEdit()
1759         layout.addWidget(QLabel(_('Message')), 1, 0)
1760         layout.addWidget(message_e, 1, 1)
1761         layout.setRowStretch(2,3)
1762
1763         address_e = QLineEdit()
1764         address_e.setText(address)
1765         layout.addWidget(QLabel(_('Address')), 2, 0)
1766         layout.addWidget(address_e, 2, 1)
1767
1768         signature_e = QTextEdit()
1769         layout.addWidget(QLabel(_('Signature')), 3, 0)
1770         layout.addWidget(signature_e, 3, 1)
1771         layout.setRowStretch(3,1)
1772
1773         hbox = QHBoxLayout()
1774
1775         b = QPushButton(_("Sign"))
1776         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1777         hbox.addWidget(b)
1778
1779         b = QPushButton(_("Verify"))
1780         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1781         hbox.addWidget(b)
1782
1783         b = QPushButton(_("Close"))
1784         b.clicked.connect(d.accept)
1785         hbox.addWidget(b)
1786         layout.addLayout(hbox, 4, 1)
1787         d.exec_()
1788
1789
1790     @protected
1791     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1792         try:
1793             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1794             message_e.setText(decrypted)
1795         except Exception as e:
1796             self.show_message(str(e))
1797
1798
1799     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1800         message = unicode(message_e.toPlainText())
1801         message = message.encode('utf-8')
1802         try:
1803             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1804             encrypted_e.setText(encrypted)
1805         except Exception as e:
1806             self.show_message(str(e))
1807
1808
1809
1810     def encrypt_message(self, address = ''):
1811         d = QDialog(self)
1812         d.setModal(1)
1813         d.setWindowTitle(_('Encrypt/decrypt Message'))
1814         d.setMinimumSize(610, 490)
1815
1816         layout = QGridLayout(d)
1817
1818         message_e = QTextEdit()
1819         layout.addWidget(QLabel(_('Message')), 1, 0)
1820         layout.addWidget(message_e, 1, 1)
1821         layout.setRowStretch(2,3)
1822
1823         pubkey_e = QLineEdit()
1824         if address:
1825             pubkey = self.wallet.getpubkeys(address)[0]
1826             pubkey_e.setText(pubkey)
1827         layout.addWidget(QLabel(_('Public key')), 2, 0)
1828         layout.addWidget(pubkey_e, 2, 1)
1829
1830         encrypted_e = QTextEdit()
1831         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1832         layout.addWidget(encrypted_e, 3, 1)
1833         layout.setRowStretch(3,1)
1834
1835         hbox = QHBoxLayout()
1836         b = QPushButton(_("Encrypt"))
1837         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1838         hbox.addWidget(b)
1839
1840         b = QPushButton(_("Decrypt"))
1841         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1842         hbox.addWidget(b)
1843
1844         b = QPushButton(_("Close"))
1845         b.clicked.connect(d.accept)
1846         hbox.addWidget(b)
1847
1848         layout.addLayout(hbox, 4, 1)
1849         d.exec_()
1850
1851
1852     def question(self, msg):
1853         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1854
1855     def show_message(self, msg):
1856         QMessageBox.information(self, _('Message'), msg, _('OK'))
1857
1858     def password_dialog(self ):
1859         d = QDialog(self)
1860         d.setModal(1)
1861         d.setWindowTitle(_("Enter Password"))
1862
1863         pw = QLineEdit()
1864         pw.setEchoMode(2)
1865
1866         vbox = QVBoxLayout()
1867         msg = _('Please enter your password')
1868         vbox.addWidget(QLabel(msg))
1869
1870         grid = QGridLayout()
1871         grid.setSpacing(8)
1872         grid.addWidget(QLabel(_('Password')), 1, 0)
1873         grid.addWidget(pw, 1, 1)
1874         vbox.addLayout(grid)
1875
1876         vbox.addLayout(ok_cancel_buttons(d))
1877         d.setLayout(vbox)
1878
1879         run_hook('password_dialog', pw, grid, 1)
1880         if not d.exec_(): return
1881         return unicode(pw.text())
1882
1883
1884
1885
1886
1887
1888
1889
1890     def tx_from_text(self, txt):
1891         "json or raw hexadecimal"
1892         try:
1893             txt.decode('hex')
1894             tx = Transaction(txt)
1895             return tx
1896         except Exception:
1897             pass
1898
1899         try:
1900             tx_dict = json.loads(str(txt))
1901             assert "hex" in tx_dict.keys()
1902             assert "complete" in tx_dict.keys()
1903             tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1904             if not tx_dict["complete"]:
1905                 assert "input_info" in tx_dict.keys()
1906                 input_info = json.loads(tx_dict['input_info'])
1907                 tx.add_input_info(input_info)
1908             return tx
1909         except Exception:
1910             pass
1911
1912         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1913
1914
1915
1916     def read_tx_from_file(self):
1917         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1918         if not fileName:
1919             return
1920         try:
1921             with open(fileName, "r") as f:
1922                 file_content = f.read()
1923         except (ValueError, IOError, os.error), reason:
1924             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1925
1926         return self.tx_from_text(file_content)
1927
1928
1929     @protected
1930     def sign_raw_transaction(self, tx, input_info, password):
1931         self.wallet.signrawtransaction(tx, input_info, [], password)
1932
1933     def do_process_from_text(self):
1934         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1935         if not text:
1936             return
1937         tx = self.tx_from_text(text)
1938         if tx:
1939             self.show_transaction(tx)
1940
1941     def do_process_from_file(self):
1942         tx = self.read_tx_from_file()
1943         if tx:
1944             self.show_transaction(tx)
1945
1946     def do_process_from_txid(self):
1947         from electrum import transaction
1948         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1949         if ok and txid:
1950             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1951             if r:
1952                 tx = transaction.Transaction(r)
1953                 if tx:
1954                     self.show_transaction(tx)
1955                 else:
1956                     self.show_message("unknown transaction")
1957
1958     def do_process_from_csvReader(self, csvReader):
1959         outputs = []
1960         errors = []
1961         errtext = ""
1962         try:
1963             for position, row in enumerate(csvReader):
1964                 address = row[0]
1965                 if not is_valid(address):
1966                     errors.append((position, address))
1967                     continue
1968                 amount = Decimal(row[1])
1969                 amount = int(100000000*amount)
1970                 outputs.append((address, amount))
1971         except (ValueError, IOError, os.error), reason:
1972             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1973             return
1974         if errors != []:
1975             for x in errors:
1976                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1977             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1978             return
1979
1980         try:
1981             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1982         except Exception as e:
1983             self.show_message(str(e))
1984             return
1985
1986         self.show_transaction(tx)
1987
1988     def do_process_from_csv_file(self):
1989         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1990         if not fileName:
1991             return
1992         try:
1993             with open(fileName, "r") as f:
1994                 csvReader = csv.reader(f)
1995                 self.do_process_from_csvReader(csvReader)
1996         except (ValueError, IOError, os.error), reason:
1997             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1998             return
1999
2000     def do_process_from_csv_text(self):
2001         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2002                                + _("Format: address, amount. One output per line"), _("Load CSV"))
2003         if not text:
2004             return
2005         f = StringIO.StringIO(text)
2006         csvReader = csv.reader(f)
2007         self.do_process_from_csvReader(csvReader)
2008
2009
2010
2011     @protected
2012     def do_export_privkeys(self, password):
2013         if not self.wallet.seed:
2014             self.show_message(_("This wallet has no seed"))
2015             return
2016
2017         self.show_message("%s\n%s\n%s" % (_("WARNING: ALL your private keys are secret."),  _("Exposing a single private key can compromise your entire wallet!"), _("In particular, DO NOT use 'redeem private key' services proposed by third parties.")))
2018
2019         try:
2020             select_export = _('Select file to export your private keys to')
2021             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
2022             if fileName:
2023                 with open(fileName, "w+") as csvfile:
2024                     transaction = csv.writer(csvfile)
2025                     transaction.writerow(["address", "private_key"])
2026
2027                     addresses = self.wallet.addresses(True)
2028
2029                     for addr in addresses:
2030                         pk = "".join(self.wallet.get_private_key(addr, password))
2031                         transaction.writerow(["%34s"%addr,pk])
2032
2033                     self.show_message(_("Private keys exported."))
2034
2035         except (IOError, os.error), reason:
2036             export_error_label = _("Electrum was unable to produce a private key-export.")
2037             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2038
2039         except Exception as e:
2040           self.show_message(str(e))
2041           return
2042
2043
2044     def do_import_labels(self):
2045         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2046         if not labelsFile: return
2047         try:
2048             f = open(labelsFile, 'r')
2049             data = f.read()
2050             f.close()
2051             for key, value in json.loads(data).items():
2052                 self.wallet.set_label(key, value)
2053             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2054         except (IOError, os.error), reason:
2055             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2056
2057
2058     def do_export_labels(self):
2059         labels = self.wallet.labels
2060         try:
2061             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2062             if fileName:
2063                 with open(fileName, 'w+') as f:
2064                     json.dump(labels, f)
2065                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2066         except (IOError, os.error), reason:
2067             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2068
2069
2070     def do_export_history(self):
2071         from lite_window import csv_transaction
2072         csv_transaction(self.wallet)
2073
2074
2075     @protected
2076     def do_import_privkey(self, password):
2077         if not self.wallet.imported_keys:
2078             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2079                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2080                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2081             if r == 4: return
2082
2083         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2084         if not text: return
2085
2086         text = str(text).split()
2087         badkeys = []
2088         addrlist = []
2089         for key in text:
2090             try:
2091                 addr = self.wallet.import_key(key, password)
2092             except Exception as e:
2093                 badkeys.append(key)
2094                 continue
2095             if not addr:
2096                 badkeys.append(key)
2097             else:
2098                 addrlist.append(addr)
2099         if addrlist:
2100             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2101         if badkeys:
2102             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2103         self.update_receive_tab()
2104         self.update_history_tab()
2105
2106
2107     def settings_dialog(self):
2108         d = QDialog(self)
2109         d.setWindowTitle(_('Electrum Settings'))
2110         d.setModal(1)
2111         vbox = QVBoxLayout()
2112         grid = QGridLayout()
2113         grid.setColumnStretch(0,1)
2114
2115         nz_label = QLabel(_('Display zeros') + ':')
2116         grid.addWidget(nz_label, 0, 0)
2117         nz_e = AmountEdit(None,True)
2118         nz_e.setText("%d"% self.num_zeros)
2119         grid.addWidget(nz_e, 0, 1)
2120         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2121         grid.addWidget(HelpButton(msg), 0, 2)
2122         if not self.config.is_modifiable('num_zeros'):
2123             for w in [nz_e, nz_label]: w.setEnabled(False)
2124
2125         lang_label=QLabel(_('Language') + ':')
2126         grid.addWidget(lang_label, 1, 0)
2127         lang_combo = QComboBox()
2128         from electrum.i18n import languages
2129         lang_combo.addItems(languages.values())
2130         try:
2131             index = languages.keys().index(self.config.get("language",''))
2132         except Exception:
2133             index = 0
2134         lang_combo.setCurrentIndex(index)
2135         grid.addWidget(lang_combo, 1, 1)
2136         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2137         if not self.config.is_modifiable('language'):
2138             for w in [lang_combo, lang_label]: w.setEnabled(False)
2139
2140
2141         fee_label = QLabel(_('Transaction fee') + ':')
2142         grid.addWidget(fee_label, 2, 0)
2143         fee_e = AmountEdit(self.base_unit)
2144         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2145         grid.addWidget(fee_e, 2, 1)
2146         msg = _('Fee per kilobyte of transaction.') + ' ' \
2147             + _('Recommended value') + ': ' + self.format_amount(20000)
2148         grid.addWidget(HelpButton(msg), 2, 2)
2149         if not self.config.is_modifiable('fee_per_kb'):
2150             for w in [fee_e, fee_label]: w.setEnabled(False)
2151
2152         units = ['BTC', 'mBTC']
2153         unit_label = QLabel(_('Base unit') + ':')
2154         grid.addWidget(unit_label, 3, 0)
2155         unit_combo = QComboBox()
2156         unit_combo.addItems(units)
2157         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2158         grid.addWidget(unit_combo, 3, 1)
2159         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2160                                              + '\n1BTC=1000mBTC.\n' \
2161                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2162
2163         usechange_cb = QCheckBox(_('Use change addresses'))
2164         usechange_cb.setChecked(self.wallet.use_change)
2165         grid.addWidget(usechange_cb, 4, 0)
2166         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2167         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2168
2169         grid.setRowStretch(5,1)
2170
2171         vbox.addLayout(grid)
2172         vbox.addLayout(ok_cancel_buttons(d))
2173         d.setLayout(vbox)
2174
2175         # run the dialog
2176         if not d.exec_(): return
2177
2178         fee = unicode(fee_e.text())
2179         try:
2180             fee = self.read_amount(fee)
2181         except Exception:
2182             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2183             return
2184
2185         self.wallet.set_fee(fee)
2186
2187         nz = unicode(nz_e.text())
2188         try:
2189             nz = int( nz )
2190             if nz>8: nz=8
2191         except Exception:
2192             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2193             return
2194
2195         if self.num_zeros != nz:
2196             self.num_zeros = nz
2197             self.config.set_key('num_zeros', nz, True)
2198             self.update_history_tab()
2199             self.update_receive_tab()
2200
2201         usechange_result = usechange_cb.isChecked()
2202         if self.wallet.use_change != usechange_result:
2203             self.wallet.use_change = usechange_result
2204             self.wallet.storage.put('use_change', self.wallet.use_change)
2205
2206         unit_result = units[unit_combo.currentIndex()]
2207         if self.base_unit() != unit_result:
2208             self.decimal_point = 8 if unit_result == 'BTC' else 5
2209             self.config.set_key('decimal_point', self.decimal_point, True)
2210             self.update_history_tab()
2211             self.update_status()
2212
2213         need_restart = False
2214
2215         lang_request = languages.keys()[lang_combo.currentIndex()]
2216         if lang_request != self.config.get('language'):
2217             self.config.set_key("language", lang_request, True)
2218             need_restart = True
2219
2220         run_hook('close_settings_dialog')
2221
2222         if need_restart:
2223             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2224
2225
2226     def run_network_dialog(self):
2227         if not self.network:
2228             return
2229         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2230
2231     def closeEvent(self, event):
2232         self.tray.hide()
2233         g = self.geometry()
2234         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2235         self.save_column_widths()
2236         self.config.set_key("console-history", self.console.history[-50:], True)
2237         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2238         event.accept()
2239
2240
2241     def plugins_dialog(self):
2242         from electrum.plugins import plugins
2243
2244         d = QDialog(self)
2245         d.setWindowTitle(_('Electrum Plugins'))
2246         d.setModal(1)
2247
2248         vbox = QVBoxLayout(d)
2249
2250         # plugins
2251         scroll = QScrollArea()
2252         scroll.setEnabled(True)
2253         scroll.setWidgetResizable(True)
2254         scroll.setMinimumSize(400,250)
2255         vbox.addWidget(scroll)
2256
2257         w = QWidget()
2258         scroll.setWidget(w)
2259         w.setMinimumHeight(len(plugins)*35)
2260
2261         grid = QGridLayout()
2262         grid.setColumnStretch(0,1)
2263         w.setLayout(grid)
2264
2265         def do_toggle(cb, p, w):
2266             r = p.toggle()
2267             cb.setChecked(r)
2268             if w: w.setEnabled(r)
2269
2270         def mk_toggle(cb, p, w):
2271             return lambda: do_toggle(cb,p,w)
2272
2273         for i, p in enumerate(plugins):
2274             try:
2275                 cb = QCheckBox(p.fullname())
2276                 cb.setDisabled(not p.is_available())
2277                 cb.setChecked(p.is_enabled())
2278                 grid.addWidget(cb, i, 0)
2279                 if p.requires_settings():
2280                     w = p.settings_widget(self)
2281                     w.setEnabled( p.is_enabled() )
2282                     grid.addWidget(w, i, 1)
2283                 else:
2284                     w = None
2285                 cb.clicked.connect(mk_toggle(cb,p,w))
2286                 grid.addWidget(HelpButton(p.description()), i, 2)
2287             except Exception:
2288                 print_msg(_("Error: cannot display plugin"), p)
2289                 traceback.print_exc(file=sys.stdout)
2290         grid.setRowStretch(i+1,1)
2291
2292         vbox.addLayout(close_button(d))
2293
2294         d.exec_()
2295
2296
2297     def show_account_details(self, k):
2298         account = self.wallet.accounts[k]
2299
2300         d = QDialog(self)
2301         d.setWindowTitle(_('Account Details'))
2302         d.setModal(1)
2303
2304         vbox = QVBoxLayout(d)
2305         name = self.wallet.get_account_name(k)
2306         label = QLabel('Name: ' + name)
2307         vbox.addWidget(label)
2308
2309         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2310
2311         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2312
2313         vbox.addWidget(QLabel(_('Master Public Key:')))
2314
2315         text = QTextEdit()
2316         text.setReadOnly(True)
2317         text.setMaximumHeight(170)
2318         vbox.addWidget(text)
2319
2320         mpk_text = '\n'.join( account.get_master_pubkeys() )
2321         text.setText(mpk_text)
2322
2323         vbox.addLayout(close_button(d))
2324         d.exec_()