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