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