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