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