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