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