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