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