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