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