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