show pruning level in network dialog (expert mode)
[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, DEFAULT_PORTS
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.')
691                     + _('Keyboard shortcut: type "!" to send all your coins.')), 3, 3)
692         
693         self.fee_e = QLineEdit()
694         grid.addWidget(QLabel(_('Fee')), 4, 0)
695         grid.addWidget(self.fee_e, 4, 1, 1, 2) 
696         grid.addWidget(HelpButton(
697                 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
698                     + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
699                     + _('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)
700         b = ''
701         if self.wallet.seed: 
702             b = EnterButton(_("Send"), self.do_send)
703         else:
704             b = EnterButton(_("Create unsigned transaction"), self.do_send)
705         grid.addWidget(b, 6, 1)
706
707         b = EnterButton(_("Clear"),self.do_clear)
708         grid.addWidget(b, 6, 2)
709
710         self.payto_sig = QLabel('')
711         grid.addWidget(self.payto_sig, 7, 0, 1, 4)
712
713         QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
714         QShortcut(QKeySequence("Down"), w, w.focusNextChild)
715         w.setLayout(grid) 
716
717         w2 = QWidget()
718         vbox = QVBoxLayout()
719         vbox.addWidget(w)
720         vbox.addStretch(1)
721         w2.setLayout(vbox)
722
723         def entry_changed( is_fee ):
724             self.funds_error = False
725
726             if self.amount_e.text() == '!':
727                 c, u = self.wallet.get_balance()
728                 inputs, total, fee = self.wallet.choose_tx_inputs( c + u, 0 )
729                 fee = self.wallet.estimated_fee(inputs)
730                 amount = c + u - fee
731                 self.amount_e.setText( str( Decimal( amount ) / 100000000 ) )
732                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
733                 return
734                 
735             amount = numbify(self.amount_e)
736             fee = numbify(self.fee_e)
737             if not is_fee: fee = None
738             if amount is None:
739                 return
740             inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
741             if not is_fee:
742                 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
743             if inputs:
744                 palette = QPalette()
745                 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
746                 text = self.status_text
747             else:
748                 palette = QPalette()
749                 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
750                 self.funds_error = True
751                 text = _( "Not enough funds" )
752
753             self.statusBar().showMessage(text)
754             self.amount_e.setPalette(palette)
755             self.fee_e.setPalette(palette)
756
757         self.amount_e.textChanged.connect(lambda: entry_changed(False) )
758         self.fee_e.textChanged.connect(lambda: entry_changed(True) )
759
760         self.run_hook('create_send_tab', grid)
761         return w2
762
763
764     def update_completions(self):
765         l = []
766         for addr,label in self.wallet.labels.items():
767             if addr in self.wallet.addressbook:
768                 l.append( label + '  <' + addr + '>')
769
770         self.run_hook('update_completions', l)
771         self.completions.setStringList(l)
772
773
774     def protected(func):
775         return lambda s, *args: s.do_protect(func, args)
776
777
778     @protected
779     def do_send(self, password):
780
781         label = unicode( self.message_e.text() )
782         r = unicode( self.payto_e.text() )
783         r = r.strip()
784
785         # label or alias, with address in brackets
786         m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
787         to_address = m.group(2) if m else r
788
789         if not is_valid(to_address):
790             QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
791             return
792
793         try:
794             amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
795         except:
796             QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
797             return
798         try:
799             fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
800         except:
801             QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
802             return
803
804         try:
805             tx = self.wallet.mktx( [(to_address, amount)], password, fee)
806         except BaseException, e:
807             self.show_message(str(e))
808             return
809
810         if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
811             QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
812             return
813
814         self.run_hook('send_tx', tx)
815
816         if label: 
817             self.set_label(tx.hash(), label)
818
819         if tx.is_complete:
820             h = self.wallet.send_tx(tx)
821             waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
822             status, msg = self.wallet.receive_tx( h )
823             if status:
824                 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
825                 self.do_clear()
826                 self.update_contacts_tab()
827             else:
828                 QMessageBox.warning(self, _('Error'), msg, _('OK'))
829         else:
830             filename = label + '.txn' if label else 'unsigned_%s.txn' % (time.mktime(time.gmtime()))
831             try:
832                 fileName = self.getSaveFileName(_("Select a transaction filename"), filename, "*.txn")
833                 with open(fileName,'w') as f:
834                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
835                 QMessageBox.information(self, _('Unsigned transaction created'), _("Unsigned transaction was saved to file:") + " " +fileName, _('OK'))
836             except:
837                 QMessageBox.warning(self, _('Error'), _('Could not write transaction to file'), _('OK'))
838
839
840
841
842     def set_url(self, url):
843         address, amount, label, message, signature, identity, url = util.parse_url(url)
844
845         if label and self.wallet.labels.get(address) != label:
846             if self.question('Give label "%s" to address %s ?'%(label,address)):
847                 if address not in self.wallet.addressbook and not self.wallet.is_mine(address):
848                     self.wallet.addressbook.append(address)
849                 self.set_label(address, label)
850
851         self.run_hook('set_url', url, self.show_message, self.question)
852
853         self.tabs.setCurrentIndex(1)
854         label = self.wallet.labels.get(address)
855         m_addr = label + '  <'+ address +'>' if label else address
856         self.payto_e.setText(m_addr)
857
858         self.message_e.setText(message)
859         self.amount_e.setText(amount)
860         if identity:
861             self.set_frozen(self.payto_e,True)
862             self.set_frozen(self.amount_e,True)
863             self.set_frozen(self.message_e,True)
864             self.payto_sig.setText( '      The bitcoin URI was signed by ' + identity )
865         else:
866             self.payto_sig.setVisible(False)
867
868     def do_clear(self):
869         self.payto_sig.setVisible(False)
870         for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
871             e.setText('')
872             self.set_frozen(e,False)
873
874     def set_frozen(self,entry,frozen):
875         if frozen:
876             entry.setReadOnly(True)
877             entry.setFrame(False)
878             palette = QPalette()
879             palette.setColor(entry.backgroundRole(), QColor('lightgray'))
880             entry.setPalette(palette)
881         else:
882             entry.setReadOnly(False)
883             entry.setFrame(True)
884             palette = QPalette()
885             palette.setColor(entry.backgroundRole(), QColor('white'))
886             entry.setPalette(palette)
887
888
889     def toggle_freeze(self,addr):
890         if not addr: return
891         if addr in self.wallet.frozen_addresses:
892             self.wallet.unfreeze(addr)
893         else:
894             self.wallet.freeze(addr)
895         self.update_receive_tab()
896
897     def toggle_priority(self,addr):
898         if not addr: return
899         if addr in self.wallet.prioritized_addresses:
900             self.wallet.unprioritize(addr)
901         else:
902             self.wallet.prioritize(addr)
903         self.update_receive_tab()
904
905
906     def create_list_tab(self, headers):
907         "generic tab creation method"
908         l = MyTreeWidget(self)
909         l.setColumnCount( len(headers) )
910         l.setHeaderLabels( headers )
911
912         w = QWidget()
913         vbox = QVBoxLayout()
914         w.setLayout(vbox)
915
916         vbox.setMargin(0)
917         vbox.setSpacing(0)
918         vbox.addWidget(l)
919         buttons = QWidget()
920         vbox.addWidget(buttons)
921
922         hbox = QHBoxLayout()
923         hbox.setMargin(0)
924         hbox.setSpacing(0)
925         buttons.setLayout(hbox)
926
927         return l,w,hbox
928
929
930     def create_receive_tab(self):
931         l,w,hbox = self.create_list_tab([ _('Address'), _('Label'), _('Balance'), _('Tx')])
932         l.setContextMenuPolicy(Qt.CustomContextMenu)
933         l.customContextMenuRequested.connect(self.create_receive_menu)
934         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
935         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
936         self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.current_item_changed(a))
937         self.receive_list = l
938         self.receive_buttons_hbox = hbox
939         hbox.addStretch(1)
940         return w
941
942
943     def receive_tab_set_mode(self, i):
944         self.save_column_widths()
945         self.expert_mode = (i == 1)
946         self.config.set_key('classic_expert_mode', self.expert_mode, True)
947         self.wallet.save()
948         self.update_receive_tab()
949
950
951     def save_column_widths(self):
952         if not self.expert_mode:
953             widths = [ self.receive_list.columnWidth(0) ]
954         else:
955             widths = []
956             for i in range(self.receive_list.columnCount() -1):
957                 widths.append(self.receive_list.columnWidth(i))
958         self.column_widths["receive"][self.expert_mode] = widths
959         
960         self.column_widths["history"] = []
961         for i in range(self.history_list.columnCount() - 1):
962             self.column_widths["history"].append(self.history_list.columnWidth(i))
963
964         self.column_widths["contacts"] = []
965         for i in range(self.contacts_list.columnCount() - 1):
966             self.column_widths["contacts"].append(self.contacts_list.columnWidth(i))
967
968
969     def create_contacts_tab(self):
970         l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
971         l.setContextMenuPolicy(Qt.CustomContextMenu)
972         l.customContextMenuRequested.connect(self.create_contact_menu)
973         for i,width in enumerate(self.column_widths['contacts']):
974             l.setColumnWidth(i, width)
975
976         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
977         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
978         self.contacts_list = l
979         self.contacts_buttons_hbox = hbox
980         hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
981         hbox.addStretch(1)
982         return w
983
984
985     def delete_imported_key(self, addr):
986         if self.question(_("Do you want to remove")+" %s "%addr +_("from your wallet?")):
987             self.wallet.imported_keys.pop(addr)
988             self.update_receive_tab()
989             self.update_history_tab()
990             self.wallet.save()
991
992
993     def create_receive_menu(self, position):
994         # fixme: this function apparently has a side effect.
995         # if it is not called the menu pops up several times
996         #self.receive_list.selectedIndexes() 
997
998         item = self.receive_list.itemAt(position)
999         if not item: return
1000         addr = unicode(item.text(0))
1001         if not is_valid(addr): 
1002             item.setExpanded(not item.isExpanded())
1003             return 
1004         menu = QMenu()
1005         menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
1006         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")) )
1007         menu.addAction(_("Edit label"), lambda: self.edit_label(True))
1008         menu.addAction(_("Private key"), lambda: self.show_private_key(addr))
1009         menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
1010         if addr in self.wallet.imported_keys:
1011             menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
1012
1013         if self.expert_mode:
1014             t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
1015             menu.addAction(t, lambda: self.toggle_freeze(addr))
1016             t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
1017             menu.addAction(t, lambda: self.toggle_priority(addr))
1018             
1019         self.run_hook('receive_menu', menu)
1020         menu.exec_(self.receive_list.viewport().mapToGlobal(position))
1021
1022
1023     def payto(self, addr):
1024         if not addr: return
1025         label = self.wallet.labels.get(addr)
1026         m_addr = label + '  <' + addr + '>' if label else addr
1027         self.tabs.setCurrentIndex(1)
1028         self.payto_e.setText(m_addr)
1029         self.amount_e.setFocus()
1030
1031
1032     def delete_contact(self, x):
1033         if self.question(_("Do you want to remove")+" %s "%x +_("from your list of contacts?")):
1034             if x in self.wallet.addressbook:
1035                 self.wallet.addressbook.remove(x)
1036                 self.set_label(x, None)
1037                 self.update_history_tab()
1038                 self.update_contacts_tab()
1039                 self.update_completions()
1040
1041
1042     def create_contact_menu(self, position):
1043         item = self.contacts_list.itemAt(position)
1044         if not item: return
1045         addr = unicode(item.text(0))
1046         label = unicode(item.text(1))
1047         is_editable = item.data(0,32).toBool()
1048         payto_addr = item.data(0,33).toString()
1049         menu = QMenu()
1050         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1051         menu.addAction(_("Pay to"), lambda: self.payto(payto_addr))
1052         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1053         if is_editable:
1054             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1055             menu.addAction(_("Delete"), lambda: self.delete_contact(addr))
1056
1057         self.run_hook('create_contact_menu', menu, item)
1058         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1059
1060
1061     def update_receive_item(self, item):
1062         item.setFont(0, QFont(MONOSPACE_FONT))
1063         address = str(item.data(0,0).toString())
1064         label = self.wallet.labels.get(address,'')
1065         item.setData(1,0,label)
1066         item.setData(0,32, True) # is editable
1067
1068         self.run_hook('update_receive_item', address, item)
1069                 
1070         c, u = self.wallet.get_addr_balance(address)
1071         balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1072         item.setData(2,0,balance)
1073
1074         if self.expert_mode:
1075             if address in self.wallet.frozen_addresses: 
1076                 item.setBackgroundColor(0, QColor('lightblue'))
1077             elif address in self.wallet.prioritized_addresses: 
1078                 item.setBackgroundColor(0, QColor('lightgreen'))
1079         
1080
1081     def update_receive_tab(self):
1082         l = self.receive_list
1083         
1084         l.clear()
1085         l.setColumnHidden(2, not self.expert_mode)
1086         l.setColumnHidden(3, not self.expert_mode)
1087         if not self.expert_mode:
1088             width = self.column_widths['receive'][0][0]
1089             l.setColumnWidth(0, width)
1090         else:
1091             for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1092                 l.setColumnWidth(i, width)        
1093
1094
1095         for k, account in self.wallet.accounts.items():
1096             name = account.get('name',str(k))
1097             c,u = self.wallet.get_account_balance(k)
1098             account_item = QTreeWidgetItem( [ name, '', format_satoshis(c+u), ''] )
1099             l.addTopLevelItem(account_item)
1100             account_item.setExpanded(True)
1101             
1102             for is_change in ([0,1] if self.expert_mode else [0]):
1103                 if self.expert_mode:
1104                     name = "Receiving" if not is_change else "Change"
1105                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1106                     account_item.addChild(seq_item)
1107                     if not is_change: seq_item.setExpanded(True)
1108                 else:
1109                     seq_item = account_item
1110                 is_red = False
1111                 gap = 0
1112
1113                 for address in account[is_change]:
1114                     h = self.wallet.history.get(address,[])
1115             
1116                     if h == []:
1117                         gap += 1
1118                         if gap > self.wallet.gap_limit:
1119                             is_red = True
1120                     else:
1121                         gap = 0
1122
1123                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1124                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1125                     self.update_receive_item(item)
1126                     if is_red:
1127                         item.setBackgroundColor(1, QColor('red'))
1128                     seq_item.addChild(item)
1129
1130         if self.wallet.imported_keys:
1131             c,u = self.wallet.get_imported_balance()
1132             account_item = QTreeWidgetItem( [ _('Imported'), '', format_satoshis(c+u), ''] )
1133             l.addTopLevelItem(account_item)
1134             account_item.setExpanded(True)
1135             for address in self.wallet.imported_keys.keys():
1136                 item = QTreeWidgetItem( [ address, '', '', ''] )
1137                 self.update_receive_item(item)
1138                 account_item.addChild(item)
1139                 
1140
1141         # we use column 1 because column 0 may be hidden
1142         l.setCurrentItem(l.topLevelItem(0),1)
1143
1144
1145     def update_contacts_tab(self):
1146
1147         l = self.contacts_list
1148         l.clear()
1149
1150         for address in self.wallet.addressbook:
1151             label = self.wallet.labels.get(address,'')
1152             n = self.wallet.get_num_tx(address)
1153             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1154             item.setFont(0, QFont(MONOSPACE_FONT))
1155             # 32 = label can be edited (bool)
1156             item.setData(0,32, True)
1157             # 33 = payto string
1158             item.setData(0,33, address)
1159             l.addTopLevelItem(item)
1160
1161         self.run_hook('update_contacts_tab', l)
1162         l.setCurrentItem(l.topLevelItem(0))
1163
1164
1165
1166     def create_console_tab(self):
1167         from qt_console import Console
1168         self.console = console = Console()
1169         self.console.history = self.config.get("console-history",[])
1170         self.console.history_index = len(self.console.history)
1171
1172         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1173         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1174
1175         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1176         methods = {}
1177         def mkfunc(f, method):
1178             return lambda *args: apply( f, (method, args, self.password_dialog ))
1179         for m in dir(c):
1180             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1181             methods[m] = mkfunc(c._run, m)
1182             
1183         console.updateNamespace(methods)
1184         return console
1185
1186
1187     def create_status_bar(self):
1188         self.status_text = ""
1189         sb = QStatusBar()
1190         sb.setFixedHeight(35)
1191         qtVersion = qVersion()
1192
1193         update_notification = UpdateLabel(self.config)
1194         if(update_notification.new_version):
1195             sb.addPermanentWidget(update_notification)
1196
1197         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1198             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1199         if self.wallet.seed:
1200             self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1201             self.password_button = StatusBarButton( self.lock_icon, _("Password"), lambda: self.change_password_dialog(self.wallet, self) )
1202             sb.addPermanentWidget( self.password_button )
1203         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1204         if self.wallet.seed:
1205             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) )
1206         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.network_dialog(self.wallet, self) ) 
1207         sb.addPermanentWidget( self.status_button )
1208
1209         self.run_hook('create_status_bar', (sb,))
1210
1211         self.setStatusBar(sb)
1212         
1213     def go_lite(self):
1214         import gui_lite
1215         self.config.set_key('gui', 'lite', True)
1216         self.hide()
1217         if self.lite:
1218             self.lite.mini.show()
1219         else:
1220             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1221             self.lite.main(None)
1222
1223     def new_contact_dialog(self):
1224         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1225         address = unicode(text)
1226         if ok:
1227             if is_valid(address):
1228                 self.wallet.addressbook.append(address)
1229                 self.wallet.save()
1230                 self.update_contacts_tab()
1231                 self.update_history_tab()
1232                 self.update_completions()
1233             else:
1234                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1235
1236     def show_master_public_key(self):
1237         dialog = QDialog(self)
1238         dialog.setModal(1)
1239         dialog.setWindowTitle(_("Master Public Key"))
1240
1241         main_text = QTextEdit()
1242         main_text.setText(self.wallet.get_master_public_key())
1243         main_text.setReadOnly(True)
1244         main_text.setMaximumHeight(170)
1245         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1246
1247         ok_button = QPushButton(_("OK"))
1248         ok_button.setDefault(True)
1249         ok_button.clicked.connect(dialog.accept)
1250
1251         main_layout = QGridLayout()
1252         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1253
1254         main_layout.addWidget(main_text, 1, 0)
1255         main_layout.addWidget(qrw, 1, 1 )
1256
1257         vbox = QVBoxLayout()
1258         vbox.addLayout(main_layout)
1259         hbox = QHBoxLayout()
1260         hbox.addStretch(1)
1261         hbox.addWidget(ok_button)
1262         vbox.addLayout(hbox)
1263
1264         dialog.setLayout(vbox)
1265         dialog.exec_()
1266         
1267
1268     @protected
1269     def show_seed_dialog(self, password):
1270         if not self.wallet.seed:
1271             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1272             return
1273         try:
1274             seed = self.wallet.decode_seed(password)
1275         except:
1276             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1277             return
1278         self.show_seed(seed, self.wallet.imported_keys, self)
1279
1280
1281     @classmethod
1282     def show_seed(self, seed, imported_keys, parent=None):
1283         dialog = QDialog(parent)
1284         dialog.setModal(1)
1285         dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
1286
1287         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1288
1289         label1 = QLabel(_("Your wallet generation seed is")+ ":")
1290
1291         seed_text = QTextEdit(brainwallet)
1292         seed_text.setReadOnly(True)
1293         seed_text.setMaximumHeight(130)
1294         
1295         msg2 =  _("Please write down or memorize these 12 words (order is important).") + " " \
1296               + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
1297               + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
1298               + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
1299         if imported_keys:
1300             msg2 += "<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"
1301         label2 = QLabel(msg2)
1302         label2.setWordWrap(True)
1303
1304         logo = QLabel()
1305         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1306         logo.setMaximumWidth(60)
1307
1308         qrw = QRCodeWidget(seed)
1309
1310         ok_button = QPushButton(_("OK"))
1311         ok_button.setDefault(True)
1312         ok_button.clicked.connect(dialog.accept)
1313
1314         grid = QGridLayout()
1315         #main_layout.addWidget(logo, 0, 0)
1316
1317         grid.addWidget(logo, 0, 0)
1318         grid.addWidget(label1, 0, 1)
1319
1320         grid.addWidget(seed_text, 1, 0, 1, 2)
1321
1322         grid.addWidget(qrw, 0, 2, 2, 1)
1323
1324         vbox = QVBoxLayout()
1325         vbox.addLayout(grid)
1326         vbox.addWidget(label2)
1327
1328         hbox = QHBoxLayout()
1329         hbox.addStretch(1)
1330         hbox.addWidget(ok_button)
1331         vbox.addLayout(hbox)
1332
1333         dialog.setLayout(vbox)
1334         dialog.exec_()
1335
1336     def show_qrcode(self, data, title = "QR code"):
1337         if not data: return
1338         d = QDialog(self)
1339         d.setModal(1)
1340         d.setWindowTitle(title)
1341         d.setMinimumSize(270, 300)
1342         vbox = QVBoxLayout()
1343         qrw = QRCodeWidget(data)
1344         vbox.addWidget(qrw, 1)
1345         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1346         hbox = QHBoxLayout()
1347         hbox.addStretch(1)
1348
1349         def print_qr(self):
1350             filename = "qrcode.bmp"
1351             bmp.save_qrcode(qrw.qr, filename)
1352             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1353
1354         b = QPushButton(_("Save"))
1355         hbox.addWidget(b)
1356         b.clicked.connect(print_qr)
1357
1358         b = QPushButton(_("Close"))
1359         hbox.addWidget(b)
1360         b.clicked.connect(d.accept)
1361         b.setDefault(True)
1362
1363         vbox.addLayout(hbox)
1364         d.setLayout(vbox)
1365         d.exec_()
1366
1367
1368     def do_protect(self, func, args):
1369         if self.wallet.use_encryption:
1370             password = self.password_dialog()
1371             if not password:
1372                 return
1373         else:
1374             password = None
1375             
1376         if args != (False,):
1377             args = (self,) + args + (password,)
1378         else:
1379             args = (self,password)
1380         apply( func, args)
1381
1382
1383     @protected
1384     def show_private_key(self, address, password):
1385         if not address: return
1386         try:
1387             pk = self.wallet.get_private_key(address, password)
1388         except BaseException, e:
1389             self.show_message(str(e))
1390             return
1391         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1392
1393
1394     @protected
1395     def do_sign(self, address, message, signature, password):
1396         try:
1397             sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1398             signature.setText(sig)
1399         except BaseException, e:
1400             self.show_message(str(e))
1401
1402     def sign_message(self, address):
1403         if not address: return
1404         d = QDialog(self)
1405         d.setModal(1)
1406         d.setWindowTitle(_('Sign Message'))
1407         d.setMinimumSize(410, 290)
1408
1409         tab_widget = QTabWidget()
1410         tab = QWidget()
1411         layout = QGridLayout(tab)
1412
1413         sign_address = QLineEdit()
1414
1415         sign_address.setText(address)
1416         layout.addWidget(QLabel(_('Address')), 1, 0)
1417         layout.addWidget(sign_address, 1, 1)
1418
1419         sign_message = QTextEdit()
1420         layout.addWidget(QLabel(_('Message')), 2, 0)
1421         layout.addWidget(sign_message, 2, 1)
1422         layout.setRowStretch(2,3)
1423
1424         sign_signature = QTextEdit()
1425         layout.addWidget(QLabel(_('Signature')), 3, 0)
1426         layout.addWidget(sign_signature, 3, 1)
1427         layout.setRowStretch(3,1)
1428
1429
1430         hbox = QHBoxLayout()
1431         b = QPushButton(_("Sign"))
1432         hbox.addWidget(b)
1433         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1434         b = QPushButton(_("Close"))
1435         b.clicked.connect(d.accept)
1436         hbox.addWidget(b)
1437         layout.addLayout(hbox, 4, 1)
1438         tab_widget.addTab(tab, _("Sign"))
1439
1440
1441         tab = QWidget()
1442         layout = QGridLayout(tab)
1443
1444         verify_address = QLineEdit()
1445         layout.addWidget(QLabel(_('Address')), 1, 0)
1446         layout.addWidget(verify_address, 1, 1)
1447
1448         verify_message = QTextEdit()
1449         layout.addWidget(QLabel(_('Message')), 2, 0)
1450         layout.addWidget(verify_message, 2, 1)
1451         layout.setRowStretch(2,3)
1452
1453         verify_signature = QTextEdit()
1454         layout.addWidget(QLabel(_('Signature')), 3, 0)
1455         layout.addWidget(verify_signature, 3, 1)
1456         layout.setRowStretch(3,1)
1457
1458         def do_verify():
1459             try:
1460                 self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
1461                 self.show_message(_("Signature verified"))
1462             except BaseException, e:
1463                 self.show_message(str(e))
1464                 return
1465
1466         hbox = QHBoxLayout()
1467         b = QPushButton(_("Verify"))
1468         b.clicked.connect(do_verify)
1469         hbox.addWidget(b)
1470         b = QPushButton(_("Close"))
1471         b.clicked.connect(d.accept)
1472         hbox.addWidget(b)
1473         layout.addLayout(hbox, 4, 1)
1474         tab_widget.addTab(tab, _("Verify"))
1475
1476         vbox = QVBoxLayout()
1477         vbox.addWidget(tab_widget)
1478         d.setLayout(vbox)
1479         d.exec_()
1480
1481         
1482
1483
1484     def question(self, msg):
1485         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1486
1487     def show_message(self, msg):
1488         QMessageBox.information(self, _('Message'), msg, _('OK'))
1489
1490     def password_dialog(self ):
1491         d = QDialog(self)
1492         d.setModal(1)
1493
1494         pw = QLineEdit()
1495         pw.setEchoMode(2)
1496
1497         vbox = QVBoxLayout()
1498         msg = _('Please enter your password')
1499         vbox.addWidget(QLabel(msg))
1500
1501         grid = QGridLayout()
1502         grid.setSpacing(8)
1503         grid.addWidget(QLabel(_('Password')), 1, 0)
1504         grid.addWidget(pw, 1, 1)
1505         vbox.addLayout(grid)
1506
1507         vbox.addLayout(ok_cancel_buttons(d))
1508         d.setLayout(vbox)
1509
1510         self.run_hook('password_dialog', pw, grid, 1)
1511         if not d.exec_(): return
1512         return unicode(pw.text())
1513
1514
1515
1516
1517
1518     @staticmethod
1519     def change_password_dialog( wallet, parent=None ):
1520
1521         if not wallet.seed:
1522             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1523             return
1524
1525         d = QDialog(parent)
1526         d.setModal(1)
1527
1528         pw = QLineEdit()
1529         pw.setEchoMode(2)
1530         new_pw = QLineEdit()
1531         new_pw.setEchoMode(2)
1532         conf_pw = QLineEdit()
1533         conf_pw.setEchoMode(2)
1534
1535         vbox = QVBoxLayout()
1536         if parent:
1537             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1538                    +_('To disable wallet encryption, enter an empty new password.')) \
1539                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1540         else:
1541             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1542                   +_("Leave these fields empty if you want to disable encryption.")
1543         vbox.addWidget(QLabel(msg))
1544
1545         grid = QGridLayout()
1546         grid.setSpacing(8)
1547
1548         if wallet.use_encryption:
1549             grid.addWidget(QLabel(_('Password')), 1, 0)
1550             grid.addWidget(pw, 1, 1)
1551
1552         grid.addWidget(QLabel(_('New Password')), 2, 0)
1553         grid.addWidget(new_pw, 2, 1)
1554
1555         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1556         grid.addWidget(conf_pw, 3, 1)
1557         vbox.addLayout(grid)
1558
1559         vbox.addLayout(ok_cancel_buttons(d))
1560         d.setLayout(vbox) 
1561
1562         if not d.exec_(): return
1563
1564         password = unicode(pw.text()) if wallet.use_encryption else None
1565         new_password = unicode(new_pw.text())
1566         new_password2 = unicode(conf_pw.text())
1567
1568         try:
1569             seed = wallet.decode_seed(password)
1570         except:
1571             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1572             return
1573
1574         if new_password != new_password2:
1575             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1576             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1577
1578         wallet.update_password(seed, password, new_password)
1579         if parent: 
1580             icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png")
1581             parent.password_button.setIcon( icon )
1582
1583
1584     @staticmethod
1585     def seed_dialog(wallet, parent=None):
1586         d = QDialog(parent)
1587         d.setModal(1)
1588
1589         vbox = QVBoxLayout()
1590         msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + '\n')
1591         vbox.addWidget(QLabel(msg))
1592
1593         grid = QGridLayout()
1594         grid.setSpacing(8)
1595
1596         seed_e = QLineEdit()
1597         grid.addWidget(QLabel(_('Seed or master public key')), 1, 0)
1598         grid.addWidget(seed_e, 1, 1)
1599         grid.addWidget(HelpButton(_("Your seed can be entered as a mnemonic (sequence of words), or as a hexadecimal string.")), 1, 3)
1600
1601         gap_e = QLineEdit()
1602         gap_e.setText("5")
1603         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1604         grid.addWidget(gap_e, 2, 1)
1605         grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
1606         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1607         vbox.addLayout(grid)
1608
1609         vbox.addLayout(ok_cancel_buttons(d))
1610         d.setLayout(vbox) 
1611
1612         if not d.exec_(): return
1613
1614         try:
1615             gap = int(unicode(gap_e.text()))
1616         except:
1617             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1618             return
1619
1620         try:
1621             seed = str(seed_e.text())
1622             seed.decode('hex')
1623         except:
1624             print_error("Warning: Not hex, trying decode")
1625             try:
1626                 seed = mnemonic.mn_decode( seed.split(' ') )
1627             except:
1628                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1629                 return
1630
1631         if not seed:
1632             QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
1633             return
1634
1635         return seed, gap
1636
1637     def generate_transaction_information_widget(self, tx):
1638         tabs = QTabWidget(self)
1639
1640         tab1 = QWidget()
1641         grid_ui = QGridLayout(tab1)
1642         grid_ui.setColumnStretch(0,1)
1643         tabs.addTab(tab1, _('Outputs') )
1644
1645         tree_widget = MyTreeWidget(self)
1646         tree_widget.setColumnCount(2)
1647         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1648         tree_widget.setColumnWidth(0, 300)
1649         tree_widget.setColumnWidth(1, 50)
1650
1651         for address, value in tx.outputs:
1652             item = QTreeWidgetItem( [address, "%s" % ( format_satoshis(value))] )
1653             tree_widget.addTopLevelItem(item)
1654
1655         tree_widget.setMaximumHeight(100)
1656
1657         grid_ui.addWidget(tree_widget)
1658
1659         tab2 = QWidget()
1660         grid_ui = QGridLayout(tab2)
1661         grid_ui.setColumnStretch(0,1)
1662         tabs.addTab(tab2, _('Inputs') )
1663         
1664         tree_widget = MyTreeWidget(self)
1665         tree_widget.setColumnCount(2)
1666         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1667
1668         for input_line in tx.inputs:
1669             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1670             tree_widget.addTopLevelItem(item)
1671
1672         tree_widget.setMaximumHeight(100)
1673
1674         grid_ui.addWidget(tree_widget)
1675         return tabs
1676
1677
1678     def tx_dict_from_text(self, txt):
1679         try:
1680             tx_dict = json.loads(str(txt))
1681             assert "hex" in tx_dict.keys()
1682             assert "complete" in tx_dict.keys()
1683             if not tx_dict["complete"]:
1684                 assert "input_info" in tx_dict.keys()
1685         except:
1686             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1687             return None
1688         return tx_dict
1689
1690
1691     def read_tx_from_file(self):
1692         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1693         if not fileName:
1694             return
1695         try:
1696             with open(fileName, "r") as f:
1697                 file_content = f.read()
1698         except (ValueError, IOError, os.error), reason:
1699             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1700
1701         return self.tx_dict_from_text(file_content)
1702
1703
1704     @protected
1705     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
1706         try:
1707             self.wallet.signrawtransaction(tx, input_info, [], password)
1708             
1709             fileName = self.getSaveFileName(_("Select where to save your signed transaction"), 'signed_%s.txn' % (tx.hash()[0:8]), "*.txn")
1710             if fileName:
1711                 with open(fileName, "w+") as f:
1712                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1713                 self.show_message(_("Transaction saved successfully"))
1714                 if dialog:
1715                     dialog.done(0)
1716         except BaseException, e:
1717             self.show_message(str(e))
1718     
1719
1720     def send_raw_transaction(self, raw_tx, dialog = ""):
1721         result, result_message = self.wallet.sendtx( raw_tx )
1722         if result:
1723             self.show_message("Transaction successfully sent: %s" % (result_message))
1724             if dialog:
1725                 dialog.done(0)
1726         else:
1727             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1728
1729     def do_process_from_text(self):
1730         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1731         if not text:
1732             return
1733         tx_dict = self.tx_dict_from_text(text)
1734         if tx_dict:
1735             self.create_process_transaction_window(tx_dict)
1736
1737     def do_process_from_file(self):
1738         tx_dict = self.read_tx_from_file()
1739         if tx_dict: 
1740             self.create_process_transaction_window(tx_dict)
1741
1742     def create_process_transaction_window(self, tx_dict):
1743         tx = Transaction(tx_dict["hex"])
1744             
1745         dialog = QDialog(self)
1746         dialog.setMinimumWidth(500)
1747         dialog.setWindowTitle(_('Process raw transaction'))
1748         dialog.setModal(1)
1749
1750         l = QGridLayout()
1751         dialog.setLayout(l)
1752
1753         l.addWidget(QLabel(_("Transaction status:")), 3,0)
1754         l.addWidget(QLabel(_("Actions")), 4,0)
1755
1756         if tx_dict["complete"] == False:
1757             l.addWidget(QLabel(_("Unsigned")), 3,1)
1758             if self.wallet.seed :
1759                 b = QPushButton("Sign transaction")
1760                 input_info = json.loads(tx_dict["input_info"])
1761                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
1762                 l.addWidget(b, 4, 1)
1763             else:
1764                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
1765         else:
1766             l.addWidget(QLabel(_("Signed")), 3,1)
1767             b = QPushButton("Broadcast transaction")
1768             b.clicked.connect(lambda: self.send_raw_transaction(tx, dialog))
1769             l.addWidget(b,4,1)
1770
1771         l.addWidget( self.generate_transaction_information_widget(tx), 0,0,2,3)
1772         cancelButton = QPushButton(_("Cancel"))
1773         cancelButton.clicked.connect(lambda: dialog.done(0))
1774         l.addWidget(cancelButton, 4,2)
1775
1776         dialog.exec_()
1777
1778
1779     @protected
1780     def do_export_privkeys(self, password):
1781         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.")))
1782
1783         try:
1784             select_export = _('Select file to export your private keys to')
1785             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1786             if fileName:
1787                 with open(fileName, "w+") as csvfile:
1788                     transaction = csv.writer(csvfile)
1789                     transaction.writerow(["address", "private_key"])
1790
1791                     
1792                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1793                         transaction.writerow(["%34s"%addr,pk])
1794
1795                     self.show_message(_("Private keys exported."))
1796
1797         except (IOError, os.error), reason:
1798             export_error_label = _("Electrum was unable to produce a private key-export.")
1799             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1800
1801         except BaseException, e:
1802           self.show_message(str(e))
1803           return
1804
1805
1806     def do_import_labels(self):
1807         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1808         if not labelsFile: return
1809         try:
1810             f = open(labelsFile, 'r')
1811             data = f.read()
1812             f.close()
1813             for key, value in json.loads(data).items():
1814                 self.wallet.labels[key] = value
1815             self.wallet.save()
1816             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1817         except (IOError, os.error), reason:
1818             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1819             
1820
1821     def do_export_labels(self):
1822         labels = self.wallet.labels
1823         try:
1824             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1825             if fileName:
1826                 with open(fileName, 'w+') as f:
1827                     json.dump(labels, f)
1828                 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1829         except (IOError, os.error), reason:
1830             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1831
1832
1833     def do_export_history(self):
1834         from gui_lite import csv_transaction
1835         csv_transaction(self.wallet)
1836
1837
1838     @protected
1839     def do_import_privkey(self, password):
1840         if not self.wallet.imported_keys:
1841             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1842                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1843                                          + _('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>' \
1844                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1845             if r == 4: return
1846
1847         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1848         if not text: return
1849
1850         text = str(text).split()
1851         badkeys = []
1852         addrlist = []
1853         for key in text:
1854             try:
1855                 addr = self.wallet.import_key(key, password)
1856             except BaseException as e:
1857                 badkeys.append(key)
1858                 continue
1859             if not addr: 
1860                 badkeys.append(key)
1861             else:
1862                 addrlist.append(addr)
1863         if addrlist:
1864             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1865         if badkeys:
1866             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1867         self.update_receive_tab()
1868         self.update_history_tab()
1869
1870
1871     def settings_dialog(self):
1872         d = QDialog(self)
1873         d.setWindowTitle(_('Electrum Settings'))
1874         d.setModal(1)
1875         vbox = QVBoxLayout()
1876
1877         tabs = QTabWidget(self)
1878         self.settings_tab = tabs
1879         vbox.addWidget(tabs)
1880
1881         tab1 = QWidget()
1882         grid_ui = QGridLayout(tab1)
1883         grid_ui.setColumnStretch(0,1)
1884         tabs.addTab(tab1, _('Display') )
1885
1886         nz_label = QLabel(_('Display zeros'))
1887         grid_ui.addWidget(nz_label, 0, 0)
1888         nz_e = QLineEdit()
1889         nz_e.setText("%d"% self.wallet.num_zeros)
1890         grid_ui.addWidget(nz_e, 0, 1)
1891         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1892         grid_ui.addWidget(HelpButton(msg), 0, 2)
1893         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1894         if not self.config.is_modifiable('num_zeros'):
1895             for w in [nz_e, nz_label]: w.setEnabled(False)
1896         
1897         lang_label=QLabel(_('Language') + ':')
1898         grid_ui.addWidget(lang_label, 1, 0)
1899         lang_combo = QComboBox()
1900         from i18n import languages
1901         lang_combo.addItems(languages.values())
1902         try:
1903             index = languages.keys().index(self.config.get("language",''))
1904         except:
1905             index = 0
1906         lang_combo.setCurrentIndex(index)
1907         grid_ui.addWidget(lang_combo, 1, 1)
1908         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1909         if not self.config.is_modifiable('language'):
1910             for w in [lang_combo, lang_label]: w.setEnabled(False)
1911
1912         currencies = self.exchanger.get_currencies()
1913         currencies.insert(0, "None")
1914
1915         cur_label=QLabel(_('Currency') + ':')
1916         grid_ui.addWidget(cur_label , 2, 0)
1917         cur_combo = QComboBox()
1918         cur_combo.addItems(currencies)
1919         try:
1920             index = currencies.index(self.config.get('currency', "None"))
1921         except:
1922             index = 0
1923         cur_combo.setCurrentIndex(index)
1924         grid_ui.addWidget(cur_combo, 2, 1)
1925         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1926         
1927         expert_cb = QCheckBox(_('Expert mode'))
1928         expert_cb.setChecked(self.expert_mode)
1929         grid_ui.addWidget(expert_cb, 3, 0)
1930         hh =  _('In expert mode, your client will:') + '\n'  \
1931             + _(' - Show change addresses in the Receive tab') + '\n'  \
1932             + _(' - Display the balance of each address') + '\n'  \
1933             + _(' - Add freeze/prioritize actions to addresses.') 
1934         grid_ui.addWidget(HelpButton(hh), 3, 2)
1935         grid_ui.setRowStretch(4,1)
1936
1937         # wallet tab
1938         tab2 = QWidget()
1939         grid_wallet = QGridLayout(tab2)
1940         grid_wallet.setColumnStretch(0,1)
1941         tabs.addTab(tab2, _('Wallet') )
1942         
1943         fee_label = QLabel(_('Transaction fee'))
1944         grid_wallet.addWidget(fee_label, 0, 0)
1945         fee_e = QLineEdit()
1946         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1947         grid_wallet.addWidget(fee_e, 0, 2)
1948         msg = _('Fee per kilobyte of transaction.') + ' ' \
1949             + _('Recommended value') + ': 0.0001'
1950         grid_wallet.addWidget(HelpButton(msg), 0, 3)
1951         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1952         if not self.config.is_modifiable('fee_per_kb'):
1953             for w in [fee_e, fee_label]: w.setEnabled(False)
1954
1955         usechange_label = QLabel(_('Use change addresses'))
1956         grid_wallet.addWidget(usechange_label, 1, 0)
1957         usechange_combo = QComboBox()
1958         usechange_combo.addItems([_('Yes'), _('No')])
1959         usechange_combo.setCurrentIndex(not self.wallet.use_change)
1960         grid_wallet.addWidget(usechange_combo, 1, 2)
1961         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1962         if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1963
1964         gap_label = QLabel(_('Gap limit'))
1965         grid_wallet.addWidget(gap_label, 2, 0)
1966         gap_e = QLineEdit()
1967         gap_e.setText("%d"% self.wallet.gap_limit)
1968         grid_wallet.addWidget(gap_e, 2, 2)
1969         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1970               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1971               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1972               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1973               + _('Warning') + ': ' \
1974               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1975               + _('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' 
1976         grid_wallet.addWidget(HelpButton(msg), 2, 3)
1977         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1978         if not self.config.is_modifiable('gap_limit'):
1979             for w in [gap_e, gap_label]: w.setEnabled(False)
1980
1981         grid_wallet.setRowStretch(3,1)
1982
1983
1984         # import/export tab
1985         tab3 = QWidget()
1986         grid_io = QGridLayout(tab3)
1987         grid_io.setColumnStretch(0,1)
1988         tabs.addTab(tab3, _('Import/Export') )
1989         
1990         grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1991         grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1992         grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1993         grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
1994
1995         grid_io.addWidget(QLabel(_('History')), 2, 0)
1996         grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1997         grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
1998
1999         grid_io.addWidget(QLabel(_('Private keys')), 3, 0)
2000
2001         grid_io.addWidget(EnterButton(_("Export"), self.do_export_privkeys), 3, 1)
2002         grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
2003         grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
2004
2005         grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
2006         grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
2007         grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
2008                               + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
2009                               + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
2010
2011
2012         grid_io.addWidget(QLabel(_("Load transaction")), 5, 0)
2013         grid_io.addWidget(EnterButton(_("From file"), self.do_process_from_file), 5, 1)
2014         grid_io.addWidget(EnterButton(_("From text"), self.do_process_from_text), 5, 2)
2015         grid_io.addWidget(HelpButton(_("This will give you the option to sign or broadcast a transaction based on it's status.")), 5, 3)
2016
2017         grid_io.setRowStretch(5,1)
2018
2019
2020         # plugins
2021         if self.plugins:
2022             tab5 = QScrollArea()
2023             grid_plugins = QGridLayout(tab5)
2024             grid_plugins.setColumnStretch(0,1)
2025             tabs.addTab(tab5, _('Plugins') )
2026             def mk_toggle(cb, p):
2027                 return lambda: cb.setChecked(p.toggle())
2028             for i, p in enumerate(self.plugins):
2029                 try:
2030                     name, description = p.get_info()
2031                     cb = QCheckBox(name)
2032                     cb.setDisabled(not p.is_available())
2033                     cb.setChecked(p.is_enabled())
2034                     cb.clicked.connect(mk_toggle(cb,p))
2035                     grid_plugins.addWidget(cb, i, 0)
2036                     if p.requires_settings():
2037                         grid_plugins.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2038                     grid_plugins.addWidget(HelpButton(description), i, 2)
2039                 except:
2040                     print_msg("Error: cannot display plugin", p)
2041                     traceback.print_exc(file=sys.stdout)
2042             grid_plugins.setRowStretch(i+1,1)
2043
2044         self.run_hook('create_settings_tab', tabs)
2045
2046         vbox.addLayout(ok_cancel_buttons(d))
2047         d.setLayout(vbox) 
2048
2049         # run the dialog
2050         if not d.exec_(): return
2051
2052         fee = unicode(fee_e.text())
2053         try:
2054             fee = int( 100000000 * Decimal(fee) )
2055         except:
2056             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2057             return
2058
2059         if self.wallet.fee != fee:
2060             self.wallet.fee = fee
2061             self.wallet.save()
2062         
2063         nz = unicode(nz_e.text())
2064         try:
2065             nz = int( nz )
2066             if nz>8: nz=8
2067         except:
2068             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2069             return
2070
2071         if self.wallet.num_zeros != nz:
2072             self.wallet.num_zeros = nz
2073             self.config.set_key('num_zeros', nz, True)
2074             self.update_history_tab()
2075             self.update_receive_tab()
2076
2077         usechange_result = usechange_combo.currentIndex() == 0
2078         if self.wallet.use_change != usechange_result:
2079             self.wallet.use_change = usechange_result
2080             self.config.set_key('use_change', self.wallet.use_change, True)
2081         
2082         try:
2083             n = int(gap_e.text())
2084         except:
2085             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2086             return
2087
2088         if self.wallet.gap_limit != n:
2089             r = self.wallet.change_gap_limit(n)
2090             if r:
2091                 self.update_receive_tab()
2092                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2093             else:
2094                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2095
2096         need_restart = False
2097
2098         lang_request = languages.keys()[lang_combo.currentIndex()]
2099         if lang_request != self.config.get('language'):
2100             self.config.set_key("language", lang_request, True)
2101             need_restart = True
2102             
2103         cur_request = str(currencies[cur_combo.currentIndex()])
2104         if cur_request != self.config.get('currency', "None"):
2105             self.config.set_key('currency', cur_request, True)
2106             self.update_wallet()
2107
2108         self.run_hook('close_settings_dialog')
2109
2110         if need_restart:
2111             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2112
2113         self.receive_tab_set_mode(expert_cb.isChecked())
2114
2115
2116     @staticmethod 
2117     def network_dialog(wallet, parent=None):
2118         interface = wallet.interface
2119         if parent:
2120             if interface.is_connected:
2121                 status = _("Connected to")+" %s\n%d "%(interface.host, wallet.verifier.height)+_("blocks")
2122             else:
2123                 status = _("Not connected")
2124             server = interface.server
2125         else:
2126             import random
2127             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
2128             server = interface.server
2129
2130         plist, servers_list = interface.get_servers_list()
2131
2132         d = QDialog(parent)
2133         d.setModal(1)
2134         d.setWindowTitle(_('Server'))
2135         d.setMinimumSize(375, 20)
2136
2137         vbox = QVBoxLayout()
2138         vbox.setSpacing(30)
2139
2140         hbox = QHBoxLayout()
2141         l = QLabel()
2142         l.setPixmap(QPixmap(":icons/network.png"))
2143         hbox.addStretch(10)
2144         hbox.addWidget(l)
2145         hbox.addWidget(QLabel(status))
2146         hbox.addStretch(50)
2147         vbox.addLayout(hbox)
2148
2149
2150         # grid layout
2151         grid = QGridLayout()
2152         grid.setSpacing(8)
2153         vbox.addLayout(grid)
2154
2155         # server
2156         server_protocol = QComboBox()
2157         server_host = QLineEdit()
2158         server_host.setFixedWidth(200)
2159         server_port = QLineEdit()
2160         server_port.setFixedWidth(60)
2161
2162         protocol_names = ['TCP', 'HTTP', 'SSL', 'HTTPS']
2163         protocol_letters = 'thsg'
2164         server_protocol.addItems(protocol_names)
2165
2166         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
2167         grid.addWidget(server_protocol, 0, 1)
2168         grid.addWidget(server_host, 0, 2)
2169         grid.addWidget(server_port, 0, 3)
2170
2171         def change_protocol(p):
2172             protocol = protocol_letters[p]
2173             host = unicode(server_host.text())
2174             pp = plist.get(host,DEFAULT_PORTS)
2175             if protocol not in pp.keys():
2176                 protocol = pp.keys()[0]
2177             port = pp[protocol]
2178             server_host.setText( host )
2179             server_port.setText( port )
2180
2181         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
2182         
2183         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
2184         servers_list_widget = QTreeWidget(parent)
2185         servers_list_widget.setHeaderLabels( [ label, _('Pruning') ] )
2186         servers_list_widget.setMaximumHeight(150)
2187         servers_list_widget.setColumnWidth(0, 240)
2188         for _host in servers_list.keys():
2189             pruning_level = servers_list[_host].get('pruning')
2190             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, pruning_level ] ))
2191         servers_list_widget.setColumnHidden(1, not parent.expert_mode if parent else True)
2192
2193         def change_server(host, protocol=None):
2194             pp = plist.get(host,DEFAULT_PORTS)
2195             if protocol:
2196                 port = pp.get(protocol)
2197                 if not port: protocol = None
2198                     
2199             if not protocol:
2200                 if 's' in pp.keys():
2201                     protocol = 's'
2202                     port = pp.get(protocol)
2203                 else:
2204                     protocol = pp.keys()[0]
2205                     port = pp.get(protocol)
2206             
2207             server_host.setText( host )
2208             server_port.setText( port )
2209             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
2210
2211             if not plist: return
2212             for p in protocol_letters:
2213                 i = protocol_letters.index(p)
2214                 j = server_protocol.model().index(i,0)
2215                 if p not in pp.keys() and interface.is_connected:
2216                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
2217                 else:
2218                     server_protocol.model().setData(j, QtCore.QVariant(33), QtCore.Qt.UserRole-1)
2219
2220
2221         if server:
2222             host, port, protocol = server.split(':')
2223             change_server(host,protocol)
2224
2225         servers_list_widget.connect(servers_list_widget, SIGNAL('currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)'), 
2226                                     lambda x,y: change_server(unicode(x.text(0))))
2227         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
2228
2229         if not wallet.config.is_modifiable('server'):
2230             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
2231
2232         # auto cycle
2233         autocycle_cb = QCheckBox(_('Try random servers if disconnected'))
2234         autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
2235         grid.addWidget(autocycle_cb, 3, 1, 3, 2)
2236         if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
2237
2238         # proxy setting
2239         proxy_mode = QComboBox()
2240         proxy_host = QLineEdit()
2241         proxy_host.setFixedWidth(200)
2242         proxy_port = QLineEdit()
2243         proxy_port.setFixedWidth(60)
2244         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
2245
2246         def check_for_disable(index = False):
2247             if proxy_mode.currentText() != 'NONE':
2248                 proxy_host.setEnabled(True)
2249                 proxy_port.setEnabled(True)
2250             else:
2251                 proxy_host.setEnabled(False)
2252                 proxy_port.setEnabled(False)
2253
2254         check_for_disable()
2255         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
2256
2257         if not wallet.config.is_modifiable('proxy'):
2258             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
2259
2260         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
2261         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
2262         proxy_host.setText(proxy_config.get("host"))
2263         proxy_port.setText(proxy_config.get("port"))
2264
2265         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
2266         grid.addWidget(proxy_mode, 2, 1)
2267         grid.addWidget(proxy_host, 2, 2)
2268         grid.addWidget(proxy_port, 2, 3)
2269
2270         # buttons
2271         vbox.addLayout(ok_cancel_buttons(d))
2272         d.setLayout(vbox) 
2273
2274         if not d.exec_(): return
2275
2276         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
2277         if proxy_mode.currentText() != 'NONE':
2278             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
2279         else:
2280             proxy = None
2281
2282         wallet.config.set_key("proxy", proxy, True)
2283         wallet.config.set_key("server", server, True)
2284         interface.set_server(server, proxy)
2285         wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
2286         return True
2287
2288     def closeEvent(self, event):
2289         g = self.geometry()
2290         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2291         self.save_column_widths()
2292         self.config.set_key("column_widths", self.column_widths, True)
2293         self.config.set_key("console-history",self.console.history[-50:])
2294         event.accept()
2295
2296
2297 class ElectrumGui:
2298
2299     def __init__(self, wallet, config, app=None):
2300         self.wallet = wallet
2301         self.config = config
2302         if app is None:
2303             self.app = QApplication(sys.argv)
2304
2305
2306     def restore_or_create(self):
2307         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2308         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2309         if r==2: return None
2310         return 'restore' if r==1 else 'create'
2311
2312     def seed_dialog(self):
2313         return ElectrumWindow.seed_dialog( self.wallet )
2314
2315     def network_dialog(self):
2316         return ElectrumWindow.network_dialog( self.wallet, parent=None )
2317         
2318
2319     def show_seed(self):
2320         ElectrumWindow.show_seed(self.wallet.seed, self.wallet.imported_keys)
2321
2322
2323     def password_dialog(self):
2324         if self.wallet.seed:
2325             ElectrumWindow.change_password_dialog(self.wallet)
2326
2327
2328     def restore_wallet(self):
2329         wallet = self.wallet
2330         # wait until we are connected, because the user might have selected another server
2331         if not wallet.interface.is_connected:
2332             waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
2333             waiting_dialog(waiting)
2334
2335         waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
2336             %(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
2337
2338         wallet.set_up_to_date(False)
2339         wallet.interface.poke('synchronizer')
2340         waiting_dialog(waiting)
2341         if wallet.is_found():
2342             print_error( "Recovery successful" )
2343         else:
2344             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2345
2346         return True
2347
2348     def main(self,url):
2349         s = Timer()
2350         s.start()
2351         w = ElectrumWindow(self.wallet, self.config)
2352         if url: w.set_url(url)
2353         w.app = self.app
2354         w.connect_slots(s)
2355         w.update_wallet()
2356         w.show()
2357
2358         self.app.exec_()
2359
2360