Bitcoin URL Handling
[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         try:
952             address, amount, label, message, signature, identity, url = util.parse_url(url)
953         except Exception:
954             QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URL'), _('OK'))
955             return
956
957         try:
958             if amount and self.base_unit() == 'mBTC': amount = str( 1000* Decimal(amount))
959             elif amount: amount = str(Decimal(amount))
960         except Exception:
961             amount = "0.0"
962             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
963
964         if self.mini:
965             self.mini.set_payment_fields(address, amount)
966
967         if label and self.wallet.labels.get(address) != label:
968             if self.question('Give label "%s" to address %s ?'%(label,address)):
969                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
970                     self.wallet.addressbook.append(address)
971                 self.wallet.set_label(address, label)
972
973         run_hook('set_url', url, self.show_message, self.question)
974
975         self.tabs.setCurrentIndex(1)
976         label = self.wallet.labels.get(address)
977         m_addr = label + '  <'+ address +'>' if label else address
978         self.payto_e.setText(m_addr)
979
980         self.message_e.setText(message)
981         if amount:
982             self.amount_e.setText(amount)
983
984         if identity:
985             self.set_frozen(self.payto_e,True)
986             self.set_frozen(self.amount_e,True)
987             self.set_frozen(self.message_e,True)
988             self.payto_sig.setText( '      '+_('The bitcoin URI was signed by')+' ' + identity )
989         else:
990             self.payto_sig.setVisible(False)
991
992     def do_clear(self):
993         self.payto_sig.setVisible(False)
994         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
995             e.setText('')
996             self.set_frozen(e,False)
997
998         self.set_pay_from([])
999         self.update_status()
1000
1001     def set_frozen(self,entry,frozen):
1002         if frozen:
1003             entry.setReadOnly(True)
1004             entry.setFrame(False)
1005             palette = QPalette()
1006             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
1007             entry.setPalette(palette)
1008         else:
1009             entry.setReadOnly(False)
1010             entry.setFrame(True)
1011             palette = QPalette()
1012             palette.setColor(entry.backgroundRole(), QColor('white'))
1013             entry.setPalette(palette)
1014
1015
1016     def set_addrs_frozen(self,addrs,freeze):
1017         for addr in addrs:
1018             if not addr: continue
1019             if addr in self.wallet.frozen_addresses and not freeze:
1020                 self.wallet.unfreeze(addr)
1021             elif addr not in self.wallet.frozen_addresses and freeze:
1022                 self.wallet.freeze(addr)
1023         self.update_receive_tab()
1024
1025
1026
1027     def create_list_tab(self, headers):
1028         "generic tab creation method"
1029         l = MyTreeWidget(self)
1030         l.setColumnCount( len(headers) )
1031         l.setHeaderLabels( headers )
1032
1033         w = QWidget()
1034         vbox = QVBoxLayout()
1035         w.setLayout(vbox)
1036
1037         vbox.setMargin(0)
1038         vbox.setSpacing(0)
1039         vbox.addWidget(l)
1040         buttons = QWidget()
1041         vbox.addWidget(buttons)
1042
1043         hbox = QHBoxLayout()
1044         hbox.setMargin(0)
1045         hbox.setSpacing(0)
1046         buttons.setLayout(hbox)
1047
1048         return l,w,hbox
1049
1050
1051     def create_receive_tab(self):
1052         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
1053         l.setContextMenuPolicy(Qt.CustomContextMenu)
1054         l.customContextMenuRequested.connect(self.create_receive_menu)
1055         l.setSelectionMode(QAbstractItemView.ExtendedSelection)
1056         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1057         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1058         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
1059         self.receive_list = l
1060         self.receive_buttons_hbox = hbox
1061         hbox.addStretch(1)
1062         return w
1063
1064
1065
1066
1067     def save_column_widths(self):
1068         self.column_widths["receive"] = []
1069         for i in range(self.receive_list.columnCount() -1):
1070             self.column_widths["receive"].append(self.receive_list.columnWidth(i))
1071
1072         self.column_widths["history"] = []
1073         for i in range(self.history_list.columnCount() - 1):
1074             self.column_widths["history"].append(self.history_list.columnWidth(i))
1075
1076         self.column_widths["contacts"] = []
1077         for i in range(self.contacts_list.columnCount() - 1):
1078             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
1079
1080         self.config.set_key("column_widths_2", self.column_widths, True)
1081
1082
1083     def create_contacts_tab(self):
1084         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
1085         l.setContextMenuPolicy(Qt.CustomContextMenu)
1086         l.customContextMenuRequested.connect(self.create_contact_menu)
1087         for i,width in enumerate(self.column_widths['contacts']):
1088             l.setColumnWidth(i, width)
1089
1090         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
1091         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
1092         self.contacts_list = l
1093         self.contacts_buttons_hbox = hbox
1094         hbox.addStretch(1)
1095         return w
1096
1097
1098     def delete_imported_key(self, addr):
1099         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
1100             self.wallet.delete_imported_key(addr)
1101             self.update_receive_tab()
1102             self.update_history_tab()
1103
1104     def edit_account_label(self, k):
1105         text, ok = QInputDialog.getText(self, _('Rename account'), _('Name') + ':', text = self.wallet.labels.get(k,''))
1106         if ok:
1107             label = unicode(text)
1108             self.wallet.set_label(k,label)
1109             self.update_receive_tab()
1110
1111     def account_set_expanded(self, item, k, b):
1112         item.setExpanded(b)
1113         self.accounts_expanded[k] = b
1114
1115     def create_account_menu(self, position, k, item):
1116         menu = QMenu()
1117         if item.isExpanded():
1118             menu.addAction(_("Minimize"), lambda: self.account_set_expanded(item, k, False))
1119         else:
1120             menu.addAction(_("Maximize"), lambda: self.account_set_expanded(item, k, True))
1121         menu.addAction(_("Rename"), lambda: self.edit_account_label(k))
1122         if self.wallet.seed_version > 4:
1123             menu.addAction(_("View details"), lambda: self.show_account_details(k))
1124         if self.wallet.account_is_pending(k):
1125             menu.addAction(_("Delete"), lambda: self.delete_pending_account(k))
1126         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1127
1128     def delete_pending_account(self, k):
1129         self.wallet.delete_pending_account(k)
1130         self.update_receive_tab()
1131
1132     def create_receive_menu(self, position):
1133         # fixme: this function apparently has a side effect.
1134         # if it is not called the menu pops up several times
1135         #self.receive_list.selectedIndexes()
1136
1137         selected = self.receive_list.selectedItems()
1138         multi_select = len(selected) > 1
1139         addrs = [unicode(item.text(0)) for item in selected]
1140         if not multi_select:
1141             item = self.receive_list.itemAt(position)
1142             if not item: return
1143
1144             addr = addrs[0]
1145             if not is_valid(addr):
1146                 k = str(item.data(0,32).toString())
1147                 if k:
1148                     self.create_account_menu(position, k, item)
1149                 else:
1150                     item.setExpanded(not item.isExpanded())
1151                 return
1152
1153         menu = QMenu()
1154         if not multi_select:
1155             menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1156             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1157             menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1158             menu.addAction(_("Public keys"), lambda: self.show_public_keys(addr))
1159             if self.wallet.seed:
1160                 menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1161                 menu.addAction(_("Sign/verify message"), lambda: self.sign_verify_message(addr))
1162                 #menu.addAction(_("Encrypt/decrypt message"), lambda: self.encrypt_message(addr))
1163             if addr in self.wallet.imported_keys:
1164                 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1165
1166         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1167             menu.addAction(_("Freeze"), lambda: self.set_addrs_frozen(addrs, True))
1168         if any(addr in self.wallet.frozen_addresses for addr in addrs):
1169             menu.addAction(_("Unfreeze"), lambda: self.set_addrs_frozen(addrs, False))
1170
1171         if any(addr not in self.wallet.frozen_addresses for addr in addrs):
1172             menu.addAction(_("Send From"), lambda: self.send_from_addresses(addrs))
1173
1174         run_hook('receive_menu', menu, addrs)
1175         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1176
1177
1178     def get_sendable_balance(self):
1179         return sum(sum(self.wallet.get_addr_balance(a)) for a in self.get_payment_sources())
1180
1181
1182     def get_payment_sources(self):
1183         if self.pay_from:
1184             return self.pay_from
1185         else:
1186             return self.wallet.get_account_addresses(self.current_account)
1187
1188
1189     def send_from_addresses(self, addrs):
1190         self.set_pay_from( addrs )
1191         self.tabs.setCurrentIndex(1)
1192
1193
1194     def payto(self, addr):
1195         if not addr: return
1196         label = self.wallet.labels.get(addr)
1197         m_addr = label + '  <' + addr + '>' if label else addr
1198         self.tabs.setCurrentIndex(1)
1199         self.payto_e.setText(m_addr)
1200         self.amount_e.setFocus()
1201
1202
1203     def delete_contact(self, x):
1204         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1205             self.wallet.delete_contact(x)
1206             self.wallet.set_label(x, None)
1207             self.update_history_tab()
1208             self.update_contacts_tab()
1209             self.update_completions()
1210
1211
1212     def create_contact_menu(self, position):
1213         item = self.contacts_list.itemAt(position)
1214         menu = QMenu()
1215         if not item:
1216             menu.addAction(_("New contact"), lambda: self.new_contact_dialog())
1217         else:
1218             addr = unicode(item.text(0))
1219             label = unicode(item.text(1))
1220             is_editable = item.data(0,32).toBool()
1221             payto_addr = item.data(0,33).toString()
1222             menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1223             menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1224             menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1225             if is_editable:
1226                 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1227                 menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1228
1229         run_hook('create_contact_menu', menu, item)
1230         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1231
1232
1233     def update_receive_item(self, item):
1234         item.setFont(0, QFont(MONOSPACE_FONT))
1235         address = str(item.data(0,0).toString())
1236         label = self.wallet.labels.get(address,'')
1237         item.setData(1,0,label)
1238         item.setData(0,32, True) # is editable
1239
1240         run_hook('update_receive_item', address, item)
1241
1242         if not self.wallet.is_mine(address): return
1243
1244         c, u = self.wallet.get_addr_balance(address)
1245         balance = self.format_amount(c + u)
1246         item.setData(2,0,balance)
1247
1248         if address in self.wallet.frozen_addresses:
1249             item.setBackgroundColor(0, QColor('lightblue'))
1250
1251
1252     def update_receive_tab(self):
1253         l = self.receive_list
1254
1255         l.clear()
1256         l.setColumnHidden(2, False)
1257         l.setColumnHidden(3, False)
1258         for i,width in enumerate(self.column_widths['receive']):
1259             l.setColumnWidth(i, width)
1260
1261         if self.current_account is None:
1262             account_items = self.wallet.accounts.items()
1263         elif self.current_account != -1:
1264             account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
1265         else:
1266             account_items = []
1267
1268         for k, account in account_items:
1269             name = self.wallet.get_account_name(k)
1270             c,u = self.wallet.get_account_balance(k)
1271             account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
1272             l.addTopLevelItem(account_item)
1273             account_item.setExpanded(self.accounts_expanded.get(k, True))
1274             account_item.setData(0, 32, k)
1275
1276             if not self.wallet.is_seeded(k):
1277                 icon = QIcon(":icons/key.png")
1278                 account_item.setIcon(0, icon)
1279
1280             for is_change in ([0,1]):
1281                 name = _("Receiving") if not is_change else _("Change")
1282                 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1283                 account_item.addChild(seq_item)
1284                 used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
1285                 used_flag = False
1286                 if not is_change: seq_item.setExpanded(True)
1287
1288                 is_red = False
1289                 gap = 0
1290
1291                 for address in account.get_addresses(is_change):
1292                     h = self.wallet.history.get(address,[])
1293
1294                     if h == []:
1295                         gap += 1
1296                         if gap > self.wallet.gap_limit:
1297                             is_red = True
1298                     else:
1299                         gap = 0
1300
1301                     c, u = self.wallet.get_addr_balance(address)
1302                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1303                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1304                     self.update_receive_item(item)
1305                     if is_red:
1306                         item.setBackgroundColor(1, QColor('red'))
1307                     if len(h) > 0 and c == -u:
1308                         if not used_flag:
1309                             seq_item.insertChild(0,used_item)
1310                             used_flag = True
1311                         used_item.addChild(item)
1312                     else:
1313                         seq_item.addChild(item)
1314
1315
1316         for k, addr in self.wallet.get_pending_accounts():
1317             name = self.wallet.labels.get(k,'')
1318             account_item = QTreeWidgetItem( [ name + "  [ "+_('pending account')+" ]", '', '', ''] )
1319             self.update_receive_item(item)
1320             l.addTopLevelItem(account_item)
1321             account_item.setExpanded(True)
1322             account_item.setData(0, 32, k)
1323             item = QTreeWidgetItem( [ addr, '', '', '', ''] )
1324             account_item.addChild(item)
1325             self.update_receive_item(item)
1326
1327
1328         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1329             c,u = self.wallet.get_imported_balance()
1330             account_item = QTreeWidgetItem( [ _('Imported'), '', self.format_amount(c+u), ''] )
1331             l.addTopLevelItem(account_item)
1332             account_item.setExpanded(True)
1333             for address in self.wallet.imported_keys.keys():
1334                 item = QTreeWidgetItem( [ address, '', '', ''] )
1335                 self.update_receive_item(item)
1336                 account_item.addChild(item)
1337
1338
1339         # we use column 1 because column 0 may be hidden
1340         l.setCurrentItem(l.topLevelItem(0),1)
1341
1342
1343     def update_contacts_tab(self):
1344         l = self.contacts_list
1345         l.clear()
1346
1347         for address in self.wallet.addressbook:
1348             label = self.wallet.labels.get(address,'')
1349             n = self.wallet.get_num_tx(address)
1350             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1351             item.setFont(0, QFont(MONOSPACE_FONT))
1352             # 32 = label can be edited (bool)
1353             item.setData(0,32, True)
1354             # 33 = payto string
1355             item.setData(0,33, address)
1356             l.addTopLevelItem(item)
1357
1358         run_hook('update_contacts_tab', l)
1359         l.setCurrentItem(l.topLevelItem(0))
1360
1361
1362
1363     def create_console_tab(self):
1364         from console import Console
1365         self.console = console = Console()
1366         return console
1367
1368
1369     def update_console(self):
1370         console = self.console
1371         console.history = self.config.get("console-history",[])
1372         console.history_index = len(console.history)
1373
1374         console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self})
1375         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1376
1377         c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True))
1378         methods = {}
1379         def mkfunc(f, method):
1380             return lambda *args: apply( f, (method, args, self.password_dialog ))
1381         for m in dir(c):
1382             if m[0]=='_' or m in ['network','wallet']: continue
1383             methods[m] = mkfunc(c._run, m)
1384
1385         console.updateNamespace(methods)
1386
1387
1388     def change_account(self,s):
1389         if s == _("All accounts"):
1390             self.current_account = None
1391         else:
1392             accounts = self.wallet.get_account_names()
1393             for k, v in accounts.items():
1394                 if v == s:
1395                     self.current_account = k
1396         self.update_history_tab()
1397         self.update_status()
1398         self.update_receive_tab()
1399
1400     def create_status_bar(self):
1401
1402         sb = QStatusBar()
1403         sb.setFixedHeight(35)
1404         qtVersion = qVersion()
1405
1406         self.balance_label = QLabel("")
1407         sb.addWidget(self.balance_label)
1408
1409         from version_getter import UpdateLabel
1410         self.updatelabel = UpdateLabel(self.config, sb)
1411
1412         self.account_selector = QComboBox()
1413         self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
1414         self.connect(self.account_selector,SIGNAL("activated(QString)"),self.change_account)
1415         sb.addPermanentWidget(self.account_selector)
1416
1417         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1418             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1419
1420         self.lock_icon = QIcon()
1421         self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
1422         sb.addPermanentWidget( self.password_button )
1423
1424         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1425         self.seed_button = StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog )
1426         sb.addPermanentWidget( self.seed_button )
1427         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), self.run_network_dialog )
1428         sb.addPermanentWidget( self.status_button )
1429
1430         run_hook('create_status_bar', (sb,))
1431
1432         self.setStatusBar(sb)
1433
1434
1435     def update_lock_icon(self):
1436         icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1437         self.password_button.setIcon( icon )
1438
1439
1440     def update_buttons_on_seed(self):
1441         if not self.wallet.is_watching_only():
1442            self.seed_button.show()
1443            self.password_button.show()
1444            self.send_button.setText(_("Send"))
1445         else:
1446            self.password_button.hide()
1447            self.seed_button.hide()
1448            self.send_button.setText(_("Create unsigned transaction"))
1449
1450
1451     def change_password_dialog(self):
1452         from password_dialog import PasswordDialog
1453         d = PasswordDialog(self.wallet, self)
1454         d.run()
1455         self.update_lock_icon()
1456
1457
1458     def new_contact_dialog(self):
1459
1460         d = QDialog(self)
1461         d.setWindowTitle(_("New Contact"))
1462         vbox = QVBoxLayout(d)
1463         vbox.addWidget(QLabel(_('New Contact')+':'))
1464
1465         grid = QGridLayout()
1466         line1 = QLineEdit()
1467         line2 = QLineEdit()
1468         grid.addWidget(QLabel(_("Address")), 1, 0)
1469         grid.addWidget(line1, 1, 1)
1470         grid.addWidget(QLabel(_("Name")), 2, 0)
1471         grid.addWidget(line2, 2, 1)
1472
1473         vbox.addLayout(grid)
1474         vbox.addLayout(ok_cancel_buttons(d))
1475
1476         if not d.exec_():
1477             return
1478
1479         address = str(line1.text())
1480         label = unicode(line2.text())
1481
1482         if not is_valid(address):
1483             QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1484             return
1485
1486         self.wallet.add_contact(address)
1487         if label:
1488             self.wallet.set_label(address, label)
1489
1490         self.update_contacts_tab()
1491         self.update_history_tab()
1492         self.update_completions()
1493         self.tabs.setCurrentIndex(3)
1494
1495
1496     @protected
1497     def new_account_dialog(self, password):
1498
1499         dialog = QDialog(self)
1500         dialog.setModal(1)
1501         dialog.setWindowTitle(_("New Account"))
1502
1503         vbox = QVBoxLayout()
1504         vbox.addWidget(QLabel(_('Account name')+':'))
1505         e = QLineEdit()
1506         vbox.addWidget(e)
1507         msg = _("Note: Newly created accounts are 'pending' until they receive bitcoins.") + " " \
1508             + _("You will need to wait for 2 confirmations until the correct balance is displayed and more addresses are created for that account.")
1509         l = QLabel(msg)
1510         l.setWordWrap(True)
1511         vbox.addWidget(l)
1512
1513         vbox.addLayout(ok_cancel_buttons(dialog))
1514         dialog.setLayout(vbox)
1515         r = dialog.exec_()
1516         if not r: return
1517
1518         name = str(e.text())
1519         if not name: return
1520
1521         self.wallet.create_pending_account('1of1', name, password)
1522         self.update_receive_tab()
1523         self.tabs.setCurrentIndex(2)
1524
1525
1526
1527     def show_master_public_key_old(self):
1528         dialog = QDialog(self)
1529         dialog.setModal(1)
1530         dialog.setWindowTitle(_("Master Public Key"))
1531
1532         main_text = QTextEdit()
1533         main_text.setText(self.wallet.get_master_public_key())
1534         main_text.setReadOnly(True)
1535         main_text.setMaximumHeight(170)
1536         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1537
1538         ok_button = QPushButton(_("OK"))
1539         ok_button.setDefault(True)
1540         ok_button.clicked.connect(dialog.accept)
1541
1542         main_layout = QGridLayout()
1543         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1544
1545         main_layout.addWidget(main_text, 1, 0)
1546         main_layout.addWidget(qrw, 1, 1 )
1547
1548         vbox = QVBoxLayout()
1549         vbox.addLayout(main_layout)
1550         vbox.addLayout(close_button(dialog))
1551         dialog.setLayout(vbox)
1552         dialog.exec_()
1553
1554
1555     def show_master_public_key(self):
1556
1557         if self.wallet.seed_version == 4:
1558             self.show_master_public_key_old()
1559             return
1560
1561         dialog = QDialog(self)
1562         dialog.setModal(1)
1563         dialog.setWindowTitle(_("Master Public Keys"))
1564
1565         mpk_text = QTextEdit()
1566         mpk_text.setReadOnly(True)
1567         mpk_text.setMaximumHeight(170)
1568         mpk_qrw = QRCodeWidget()
1569
1570         main_layout = QGridLayout()
1571
1572         main_layout.addWidget(QLabel(_('Key')), 1, 0)
1573         main_layout.addWidget(mpk_text, 1, 1)
1574         main_layout.addWidget(mpk_qrw, 1, 2)
1575
1576         def update(key):
1577             xpub = self.wallet.master_public_keys[str(key)]
1578             mpk_text.setText(xpub)
1579             mpk_qrw.set_addr(xpub)
1580             mpk_qrw.update_qr()
1581
1582         key_selector = QComboBox()
1583         keys = sorted(self.wallet.master_public_keys.keys())
1584         key_selector.addItems(keys)
1585
1586         main_layout.addWidget(QLabel(_('Derivation:')), 0, 0)
1587         main_layout.addWidget(key_selector, 0, 1)
1588         dialog.connect(key_selector,SIGNAL("activated(QString)"),update)
1589
1590         update(keys[0])
1591
1592         vbox = QVBoxLayout()
1593         vbox.addLayout(main_layout)
1594         vbox.addLayout(close_button(dialog))
1595
1596         dialog.setLayout(vbox)
1597         dialog.exec_()
1598
1599
1600     @protected
1601     def show_seed_dialog(self, password):
1602         if self.wallet.is_watching_only():
1603             QMessageBox.information(self, _('Message'), _('This is a watching-only wallet'), _('OK'))
1604             return
1605
1606         if self.wallet.seed:
1607             try:
1608                 mnemonic = self.wallet.get_mnemonic(password)
1609             except Exception:
1610                 QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1611                 return
1612             from seed_dialog import SeedDialog
1613             d = SeedDialog(self, mnemonic, self.wallet.imported_keys)
1614             d.exec_()
1615         else:
1616             l = {}
1617             for k in self.wallet.master_private_keys.keys():
1618                 pk = self.wallet.get_master_private_key(k, password)
1619                 l[k] = pk
1620             from seed_dialog import PrivateKeysDialog
1621             d = PrivateKeysDialog(self,l)
1622             d.exec_()
1623
1624
1625
1626
1627
1628     def show_qrcode(self, data, title = _("QR code")):
1629         if not data: return
1630         d = QDialog(self)
1631         d.setModal(1)
1632         d.setWindowTitle(title)
1633         d.setMinimumSize(270, 300)
1634         vbox = QVBoxLayout()
1635         qrw = QRCodeWidget(data)
1636         vbox.addWidget(qrw, 1)
1637         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1638         hbox = QHBoxLayout()
1639         hbox.addStretch(1)
1640
1641         filename = os.path.join(self.config.path, "qrcode.bmp")
1642
1643         def print_qr():
1644             bmp.save_qrcode(qrw.qr, filename)
1645             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1646
1647         def copy_to_clipboard():
1648             bmp.save_qrcode(qrw.qr, filename)
1649             self.app.clipboard().setImage(QImage(filename))
1650             QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
1651
1652         b = QPushButton(_("Copy"))
1653         hbox.addWidget(b)
1654         b.clicked.connect(copy_to_clipboard)
1655
1656         b = QPushButton(_("Save"))
1657         hbox.addWidget(b)
1658         b.clicked.connect(print_qr)
1659
1660         b = QPushButton(_("Close"))
1661         hbox.addWidget(b)
1662         b.clicked.connect(d.accept)
1663         b.setDefault(True)
1664
1665         vbox.addLayout(hbox)
1666         d.setLayout(vbox)
1667         d.exec_()
1668
1669
1670     def do_protect(self, func, args):
1671         if self.wallet.use_encryption:
1672             password = self.password_dialog()
1673             if not password:
1674                 return
1675         else:
1676             password = None
1677
1678         if args != (False,):
1679             args = (self,) + args + (password,)
1680         else:
1681             args = (self,password)
1682         apply( func, args)
1683
1684
1685     def show_public_keys(self, address):
1686         if not address: return
1687         try:
1688             pubkey_list = self.wallet.get_public_keys(address)
1689         except Exception as e:
1690             traceback.print_exc(file=sys.stdout)
1691             self.show_message(str(e))
1692             return
1693
1694         d = QDialog(self)
1695         d.setMinimumSize(600, 200)
1696         d.setModal(1)
1697         vbox = QVBoxLayout()
1698         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1699         vbox.addWidget( QLabel(_("Public key") + ':'))
1700         keys = QTextEdit()
1701         keys.setReadOnly(True)
1702         keys.setText('\n'.join(pubkey_list))
1703         vbox.addWidget(keys)
1704         #vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1705         vbox.addLayout(close_button(d))
1706         d.setLayout(vbox)
1707         d.exec_()
1708
1709     @protected
1710     def show_private_key(self, address, password):
1711         if not address: return
1712         try:
1713             pk_list = self.wallet.get_private_key(address, password)
1714         except Exception as e:
1715             traceback.print_exc(file=sys.stdout)
1716             self.show_message(str(e))
1717             return
1718
1719         d = QDialog(self)
1720         d.setMinimumSize(600, 200)
1721         d.setModal(1)
1722         vbox = QVBoxLayout()
1723         vbox.addWidget( QLabel(_("Address") + ': ' + address))
1724         vbox.addWidget( QLabel(_("Private key") + ':'))
1725         keys = QTextEdit()
1726         keys.setReadOnly(True)
1727         keys.setText('\n'.join(pk_list))
1728         vbox.addWidget(keys)
1729         vbox.addWidget( QRCodeWidget('\n'.join(pk_list)) )
1730         vbox.addLayout(close_button(d))
1731         d.setLayout(vbox)
1732         d.exec_()
1733
1734
1735     @protected
1736     def do_sign(self, address, message, signature, password):
1737         message = unicode(message.toPlainText())
1738         message = message.encode('utf-8')
1739         try:
1740             sig = self.wallet.sign_message(str(address.text()), message, password)
1741             signature.setText(sig)
1742         except Exception as e:
1743             self.show_message(str(e))
1744
1745     def do_verify(self, address, message, signature):
1746         message = unicode(message.toPlainText())
1747         message = message.encode('utf-8')
1748         if bitcoin.verify_message(address.text(), str(signature.toPlainText()), message):
1749             self.show_message(_("Signature verified"))
1750         else:
1751             self.show_message(_("Error: wrong signature"))
1752
1753
1754     def sign_verify_message(self, address=''):
1755         d = QDialog(self)
1756         d.setModal(1)
1757         d.setWindowTitle(_('Sign/verify Message'))
1758         d.setMinimumSize(410, 290)
1759
1760         layout = QGridLayout(d)
1761
1762         message_e = QTextEdit()
1763         layout.addWidget(QLabel(_('Message')), 1, 0)
1764         layout.addWidget(message_e, 1, 1)
1765         layout.setRowStretch(2,3)
1766
1767         address_e = QLineEdit()
1768         address_e.setText(address)
1769         layout.addWidget(QLabel(_('Address')), 2, 0)
1770         layout.addWidget(address_e, 2, 1)
1771
1772         signature_e = QTextEdit()
1773         layout.addWidget(QLabel(_('Signature')), 3, 0)
1774         layout.addWidget(signature_e, 3, 1)
1775         layout.setRowStretch(3,1)
1776
1777         hbox = QHBoxLayout()
1778
1779         b = QPushButton(_("Sign"))
1780         b.clicked.connect(lambda: self.do_sign(address_e, message_e, signature_e))
1781         hbox.addWidget(b)
1782
1783         b = QPushButton(_("Verify"))
1784         b.clicked.connect(lambda: self.do_verify(address_e, message_e, signature_e))
1785         hbox.addWidget(b)
1786
1787         b = QPushButton(_("Close"))
1788         b.clicked.connect(d.accept)
1789         hbox.addWidget(b)
1790         layout.addLayout(hbox, 4, 1)
1791         d.exec_()
1792
1793
1794     @protected
1795     def do_decrypt(self, message_e, pubkey_e, encrypted_e, password):
1796         try:
1797             decrypted = self.wallet.decrypt_message(str(pubkey_e.text()), str(encrypted_e.toPlainText()), password)
1798             message_e.setText(decrypted)
1799         except Exception as e:
1800             self.show_message(str(e))
1801
1802
1803     def do_encrypt(self, message_e, pubkey_e, encrypted_e):
1804         message = unicode(message_e.toPlainText())
1805         message = message.encode('utf-8')
1806         try:
1807             encrypted = bitcoin.encrypt_message(message, str(pubkey_e.text()))
1808             encrypted_e.setText(encrypted)
1809         except Exception as e:
1810             self.show_message(str(e))
1811
1812
1813
1814     def encrypt_message(self, address = ''):
1815         d = QDialog(self)
1816         d.setModal(1)
1817         d.setWindowTitle(_('Encrypt/decrypt Message'))
1818         d.setMinimumSize(610, 490)
1819
1820         layout = QGridLayout(d)
1821
1822         message_e = QTextEdit()
1823         layout.addWidget(QLabel(_('Message')), 1, 0)
1824         layout.addWidget(message_e, 1, 1)
1825         layout.setRowStretch(2,3)
1826
1827         pubkey_e = QLineEdit()
1828         if address:
1829             pubkey = self.wallet.getpubkeys(address)[0]
1830             pubkey_e.setText(pubkey)
1831         layout.addWidget(QLabel(_('Public key')), 2, 0)
1832         layout.addWidget(pubkey_e, 2, 1)
1833
1834         encrypted_e = QTextEdit()
1835         layout.addWidget(QLabel(_('Encrypted')), 3, 0)
1836         layout.addWidget(encrypted_e, 3, 1)
1837         layout.setRowStretch(3,1)
1838
1839         hbox = QHBoxLayout()
1840         b = QPushButton(_("Encrypt"))
1841         b.clicked.connect(lambda: self.do_encrypt(message_e, pubkey_e, encrypted_e))
1842         hbox.addWidget(b)
1843
1844         b = QPushButton(_("Decrypt"))
1845         b.clicked.connect(lambda: self.do_decrypt(message_e, pubkey_e, encrypted_e))
1846         hbox.addWidget(b)
1847
1848         b = QPushButton(_("Close"))
1849         b.clicked.connect(d.accept)
1850         hbox.addWidget(b)
1851
1852         layout.addLayout(hbox, 4, 1)
1853         d.exec_()
1854
1855
1856     def question(self, msg):
1857         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1858
1859     def show_message(self, msg):
1860         QMessageBox.information(self, _('Message'), msg, _('OK'))
1861
1862     def password_dialog(self ):
1863         d = QDialog(self)
1864         d.setModal(1)
1865         d.setWindowTitle(_("Enter Password"))
1866
1867         pw = QLineEdit()
1868         pw.setEchoMode(2)
1869
1870         vbox = QVBoxLayout()
1871         msg = _('Please enter your password')
1872         vbox.addWidget(QLabel(msg))
1873
1874         grid = QGridLayout()
1875         grid.setSpacing(8)
1876         grid.addWidget(QLabel(_('Password')), 1, 0)
1877         grid.addWidget(pw, 1, 1)
1878         vbox.addLayout(grid)
1879
1880         vbox.addLayout(ok_cancel_buttons(d))
1881         d.setLayout(vbox)
1882
1883         run_hook('password_dialog', pw, grid, 1)
1884         if not d.exec_(): return
1885         return unicode(pw.text())
1886
1887
1888
1889
1890
1891
1892
1893
1894     def tx_from_text(self, txt):
1895         "json or raw hexadecimal"
1896         try:
1897             txt.decode('hex')
1898             tx = Transaction(txt)
1899             return tx
1900         except Exception:
1901             pass
1902
1903         try:
1904             tx_dict = json.loads(str(txt))
1905             assert "hex" in tx_dict.keys()
1906             assert "complete" in tx_dict.keys()
1907             tx = Transaction(tx_dict["hex"], tx_dict["complete"])
1908             if not tx_dict["complete"]:
1909                 assert "input_info" in tx_dict.keys()
1910                 input_info = json.loads(tx_dict['input_info'])
1911                 tx.add_input_info(input_info)
1912             return tx
1913         except Exception:
1914             pass
1915
1916         QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
1917
1918
1919
1920     def read_tx_from_file(self):
1921         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1922         if not fileName:
1923             return
1924         try:
1925             with open(fileName, "r") as f:
1926                 file_content = f.read()
1927         except (ValueError, IOError, os.error), reason:
1928             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1929
1930         return self.tx_from_text(file_content)
1931
1932
1933     @protected
1934     def sign_raw_transaction(self, tx, input_info, password):
1935         self.wallet.signrawtransaction(tx, input_info, [], password)
1936
1937     def do_process_from_text(self):
1938         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1939         if not text:
1940             return
1941         tx = self.tx_from_text(text)
1942         if tx:
1943             self.show_transaction(tx)
1944
1945     def do_process_from_file(self):
1946         tx = self.read_tx_from_file()
1947         if tx:
1948             self.show_transaction(tx)
1949
1950     def do_process_from_txid(self):
1951         from electrum import transaction
1952         txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':')
1953         if ok and txid:
1954             r = self.network.synchronous_get([ ('blockchain.transaction.get',[str(txid)]) ])[0]
1955             if r:
1956                 tx = transaction.Transaction(r)
1957                 if tx:
1958                     self.show_transaction(tx)
1959                 else:
1960                     self.show_message("unknown transaction")
1961
1962     def do_process_from_csvReader(self, csvReader):
1963         outputs = []
1964         errors = []
1965         errtext = ""
1966         try:
1967             for position, row in enumerate(csvReader):
1968                 address = row[0]
1969                 if not is_valid(address):
1970                     errors.append((position, address))
1971                     continue
1972                 amount = Decimal(row[1])
1973                 amount = int(100000000*amount)
1974                 outputs.append((address, amount))
1975         except (ValueError, IOError, os.error), reason:
1976             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1977             return
1978         if errors != []:
1979             for x in errors:
1980                 errtext += "CSV Row " + str(x[0]+1) + ": " + x[1] + "\n"
1981             QMessageBox.critical(None, _("Invalid Addresses"), _("ABORTING! Invalid Addresses found:") + "\n\n" + errtext)
1982             return
1983
1984         try:
1985             tx = self.wallet.make_unsigned_transaction(outputs, None, None)
1986         except Exception as e:
1987             self.show_message(str(e))
1988             return
1989
1990         self.show_transaction(tx)
1991
1992     def do_process_from_csv_file(self):
1993         fileName = self.getOpenFileName(_("Select your transaction CSV"), "*.csv")
1994         if not fileName:
1995             return
1996         try:
1997             with open(fileName, "r") as f:
1998                 csvReader = csv.reader(f)
1999                 self.do_process_from_csvReader(csvReader)
2000         except (ValueError, IOError, os.error), reason:
2001             QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
2002             return
2003
2004     def do_process_from_csv_text(self):
2005         text = text_dialog(self, _('Input CSV'), _("Please enter a list of outputs.") + '\n' \
2006                                + _("Format: address, amount. One output per line"), _("Load CSV"))
2007         if not text:
2008             return
2009         f = StringIO.StringIO(text)
2010         csvReader = csv.reader(f)
2011         self.do_process_from_csvReader(csvReader)
2012
2013
2014
2015     @protected
2016     def do_export_privkeys(self, password):
2017         if not self.wallet.seed:
2018             self.show_message(_("This wallet has no seed"))
2019             return
2020
2021         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.")))
2022
2023         try:
2024             select_export = _('Select file to export your private keys to')
2025             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
2026             if fileName:
2027                 with open(fileName, "w+") as csvfile:
2028                     transaction = csv.writer(csvfile)
2029                     transaction.writerow(["address", "private_key"])
2030
2031                     addresses = self.wallet.addresses(True)
2032
2033                     for addr in addresses:
2034                         pk = "".join(self.wallet.get_private_key(addr, password))
2035                         transaction.writerow(["%34s"%addr,pk])
2036
2037                     self.show_message(_("Private keys exported."))
2038
2039         except (IOError, os.error), reason:
2040             export_error_label = _("Electrum was unable to produce a private key-export.")
2041             QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
2042
2043         except Exception as e:
2044           self.show_message(str(e))
2045           return
2046
2047
2048     def do_import_labels(self):
2049         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
2050         if not labelsFile: return
2051         try:
2052             f = open(labelsFile, 'r')
2053             data = f.read()
2054             f.close()
2055             for key, value in json.loads(data).items():
2056                 self.wallet.set_label(key, value)
2057             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
2058         except (IOError, os.error), reason:
2059             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
2060
2061
2062     def do_export_labels(self):
2063         labels = self.wallet.labels
2064         try:
2065             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
2066             if fileName:
2067                 with open(fileName, 'w+') as f:
2068                     json.dump(labels, f)
2069                 QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
2070         except (IOError, os.error), reason:
2071             QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
2072
2073
2074     def do_export_history(self):
2075         from lite_window import csv_transaction
2076         csv_transaction(self.wallet)
2077
2078
2079     @protected
2080     def do_import_privkey(self, password):
2081         if not self.wallet.imported_keys:
2082             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
2083                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
2084                                          + _('Are you sure you understand what you are doing?'), 3, 4)
2085             if r == 4: return
2086
2087         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
2088         if not text: return
2089
2090         text = str(text).split()
2091         badkeys = []
2092         addrlist = []
2093         for key in text:
2094             try:
2095                 addr = self.wallet.import_key(key, password)
2096             except Exception as e:
2097                 badkeys.append(key)
2098                 continue
2099             if not addr:
2100                 badkeys.append(key)
2101             else:
2102                 addrlist.append(addr)
2103         if addrlist:
2104             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
2105         if badkeys:
2106             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
2107         self.update_receive_tab()
2108         self.update_history_tab()
2109
2110
2111     def settings_dialog(self):
2112         d = QDialog(self)
2113         d.setWindowTitle(_('Electrum Settings'))
2114         d.setModal(1)
2115         vbox = QVBoxLayout()
2116         grid = QGridLayout()
2117         grid.setColumnStretch(0,1)
2118
2119         nz_label = QLabel(_('Display zeros') + ':')
2120         grid.addWidget(nz_label, 0, 0)
2121         nz_e = AmountEdit(None,True)
2122         nz_e.setText("%d"% self.num_zeros)
2123         grid.addWidget(nz_e, 0, 1)
2124         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
2125         grid.addWidget(HelpButton(msg), 0, 2)
2126         if not self.config.is_modifiable('num_zeros'):
2127             for w in [nz_e, nz_label]: w.setEnabled(False)
2128
2129         lang_label=QLabel(_('Language') + ':')
2130         grid.addWidget(lang_label, 1, 0)
2131         lang_combo = QComboBox()
2132         from electrum.i18n import languages
2133         lang_combo.addItems(languages.values())
2134         try:
2135             index = languages.keys().index(self.config.get("language",''))
2136         except Exception:
2137             index = 0
2138         lang_combo.setCurrentIndex(index)
2139         grid.addWidget(lang_combo, 1, 1)
2140         grid.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
2141         if not self.config.is_modifiable('language'):
2142             for w in [lang_combo, lang_label]: w.setEnabled(False)
2143
2144
2145         fee_label = QLabel(_('Transaction fee') + ':')
2146         grid.addWidget(fee_label, 2, 0)
2147         fee_e = AmountEdit(self.base_unit)
2148         fee_e.setText(self.format_amount(self.wallet.fee).strip())
2149         grid.addWidget(fee_e, 2, 1)
2150         msg = _('Fee per kilobyte of transaction.') + ' ' \
2151             + _('Recommended value') + ': ' + self.format_amount(20000)
2152         grid.addWidget(HelpButton(msg), 2, 2)
2153         if not self.config.is_modifiable('fee_per_kb'):
2154             for w in [fee_e, fee_label]: w.setEnabled(False)
2155
2156         units = ['BTC', 'mBTC']
2157         unit_label = QLabel(_('Base unit') + ':')
2158         grid.addWidget(unit_label, 3, 0)
2159         unit_combo = QComboBox()
2160         unit_combo.addItems(units)
2161         unit_combo.setCurrentIndex(units.index(self.base_unit()))
2162         grid.addWidget(unit_combo, 3, 1)
2163         grid.addWidget(HelpButton(_('Base unit of your wallet.')\
2164                                              + '\n1BTC=1000mBTC.\n' \
2165                                              + _(' These settings affects the fields in the Send tab')+' '), 3, 2)
2166
2167         usechange_cb = QCheckBox(_('Use change addresses'))
2168         usechange_cb.setChecked(self.wallet.use_change)
2169         grid.addWidget(usechange_cb, 4, 0)
2170         grid.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 4, 2)
2171         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
2172
2173         grid.setRowStretch(5,1)
2174
2175         vbox.addLayout(grid)
2176         vbox.addLayout(ok_cancel_buttons(d))
2177         d.setLayout(vbox)
2178
2179         # run the dialog
2180         if not d.exec_(): return
2181
2182         fee = unicode(fee_e.text())
2183         try:
2184             fee = self.read_amount(fee)
2185         except Exception:
2186             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2187             return
2188
2189         self.wallet.set_fee(fee)
2190
2191         nz = unicode(nz_e.text())
2192         try:
2193             nz = int( nz )
2194             if nz>8: nz=8
2195         except Exception:
2196             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2197             return
2198
2199         if self.num_zeros != nz:
2200             self.num_zeros = nz
2201             self.config.set_key('num_zeros', nz, True)
2202             self.update_history_tab()
2203             self.update_receive_tab()
2204
2205         usechange_result = usechange_cb.isChecked()
2206         if self.wallet.use_change != usechange_result:
2207             self.wallet.use_change = usechange_result
2208             self.wallet.storage.put('use_change', self.wallet.use_change)
2209
2210         unit_result = units[unit_combo.currentIndex()]
2211         if self.base_unit() != unit_result:
2212             self.decimal_point = 8 if unit_result == 'BTC' else 5
2213             self.config.set_key('decimal_point', self.decimal_point, True)
2214             self.update_history_tab()
2215             self.update_status()
2216
2217         need_restart = False
2218
2219         lang_request = languages.keys()[lang_combo.currentIndex()]
2220         if lang_request != self.config.get('language'):
2221             self.config.set_key("language", lang_request, True)
2222             need_restart = True
2223
2224         run_hook('close_settings_dialog')
2225
2226         if need_restart:
2227             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2228
2229
2230     def run_network_dialog(self):
2231         if not self.network:
2232             return
2233         NetworkDialog(self.wallet.network, self.config, self).do_exec()
2234
2235     def closeEvent(self, event):
2236         self.tray.hide()
2237         g = self.geometry()
2238         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2239         self.save_column_widths()
2240         self.config.set_key("console-history", self.console.history[-50:], True)
2241         self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
2242         event.accept()
2243
2244
2245     def plugins_dialog(self):
2246         from electrum.plugins import plugins
2247
2248         d = QDialog(self)
2249         d.setWindowTitle(_('Electrum Plugins'))
2250         d.setModal(1)
2251
2252         vbox = QVBoxLayout(d)
2253
2254         # plugins
2255         scroll = QScrollArea()
2256         scroll.setEnabled(True)
2257         scroll.setWidgetResizable(True)
2258         scroll.setMinimumSize(400,250)
2259         vbox.addWidget(scroll)
2260
2261         w = QWidget()
2262         scroll.setWidget(w)
2263         w.setMinimumHeight(len(plugins)*35)
2264
2265         grid = QGridLayout()
2266         grid.setColumnStretch(0,1)
2267         w.setLayout(grid)
2268
2269         def do_toggle(cb, p, w):
2270             r = p.toggle()
2271             cb.setChecked(r)
2272             if w: w.setEnabled(r)
2273
2274         def mk_toggle(cb, p, w):
2275             return lambda: do_toggle(cb,p,w)
2276
2277         for i, p in enumerate(plugins):
2278             try:
2279                 cb = QCheckBox(p.fullname())
2280                 cb.setDisabled(not p.is_available())
2281                 cb.setChecked(p.is_enabled())
2282                 grid.addWidget(cb, i, 0)
2283                 if p.requires_settings():
2284                     w = p.settings_widget(self)
2285                     w.setEnabled( p.is_enabled() )
2286                     grid.addWidget(w, i, 1)
2287                 else:
2288                     w = None
2289                 cb.clicked.connect(mk_toggle(cb,p,w))
2290                 grid.addWidget(HelpButton(p.description()), i, 2)
2291             except Exception:
2292                 print_msg(_("Error: cannot display plugin"), p)
2293                 traceback.print_exc(file=sys.stdout)
2294         grid.setRowStretch(i+1,1)
2295
2296         vbox.addLayout(close_button(d))
2297
2298         d.exec_()
2299
2300
2301     def show_account_details(self, k):
2302         account = self.wallet.accounts[k]
2303
2304         d = QDialog(self)
2305         d.setWindowTitle(_('Account Details'))
2306         d.setModal(1)
2307
2308         vbox = QVBoxLayout(d)
2309         name = self.wallet.get_account_name(k)
2310         label = QLabel('Name: ' + name)
2311         vbox.addWidget(label)
2312
2313         vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
2314
2315         vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
2316
2317         vbox.addWidget(QLabel(_('Master Public Key:')))
2318
2319         text = QTextEdit()
2320         text.setReadOnly(True)
2321         text.setMaximumHeight(170)
2322         vbox.addWidget(text)
2323
2324         mpk_text = '\n'.join( account.get_master_pubkeys() )
2325         text.setText(mpk_text)
2326
2327         vbox.addLayout(close_button(d))
2328         d.exec_()