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