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