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