Merge branch 'master' into feature/label_sync
[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.create_status_bar()
277
278         self.wallet.interface.register_callback('updated', lambda: self.emit(QtCore.SIGNAL('update_wallet')))
279         self.wallet.interface.register_callback('banner', lambda: self.emit(QtCore.SIGNAL('banner_signal')))
280         self.wallet.interface.register_callback('disconnected', lambda: self.emit(QtCore.SIGNAL('update_status')))
281         self.wallet.interface.register_callback('disconnecting', lambda: self.emit(QtCore.SIGNAL('update_status')))
282
283         self.expert_mode = config.get('classic_expert_mode', False)
284
285         set_language(config.get('language'))
286
287         self.funds_error = False
288         self.completions = QStringListModel()
289
290         self.tabs = tabs = QTabWidget(self)
291         self.column_widths = self.config.get("column-widths", default_column_widths )
292         tabs.addTab(self.create_history_tab(), _('History') )
293         tabs.addTab(self.create_send_tab(), _('Send') )
294         tabs.addTab(self.create_receive_tab(), _('Receive') )
295         tabs.addTab(self.create_contacts_tab(), _('Contacts') )
296         tabs.addTab(self.create_console_tab(), _('Console') )
297         tabs.setMinimumSize(600, 400)
298         tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
299         self.setCentralWidget(tabs)
300
301         g = self.config.get("winpos-qt",[100, 100, 840, 400])
302         self.setGeometry(g[0], g[1], g[2], g[3])
303         title = 'Electrum ' + self.wallet.electrum_version + '  -  ' + self.config.path
304         if not self.wallet.seed: title += ' [%s]' % (_('seedless'))
305         self.setWindowTitle( title )
306
307         QShortcut(QKeySequence("Ctrl+W"), self, self.close)
308         QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
309         QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
310         QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
311         
312         self.connect(self, QtCore.SIGNAL('update_wallet'), self.update_wallet)
313         self.connect(self, QtCore.SIGNAL('update_status'), self.update_status)
314         self.connect(self, QtCore.SIGNAL('banner_signal'), lambda: self.console.showMessage(self.wallet.banner) )
315         self.history_list.setFocus(True)
316         
317         self.exchanger = exchange_rate.Exchanger(self)
318         self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
319
320         # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
321         if platform.system() == 'Windows':
322             n = 3 if self.wallet.seed else 2
323             tabs.setCurrentIndex (n)
324             tabs.setCurrentIndex (0)
325
326         # set initial message
327         self.console.showMessage(self.wallet.banner)
328
329
330     # plugins
331     def init_plugins(self):
332         import imp, pkgutil
333         if os.path.exists("plugins"):
334             fp, pathname, description = imp.find_module('plugins')
335             imp.load_module('electrum_plugins', fp, pathname, description)
336             plugin_names = [name for a, name, b in pkgutil.iter_modules(['plugins'])]
337             self.plugins = map(lambda name: imp.load_source('electrum_plugins.'+name, os.path.join(pathname,name+'.py')), plugin_names)
338         else:
339             import electrum_plugins
340             plugin_names = [name for a, name, b in pkgutil.iter_modules(electrum_plugins.__path__)]
341             self.plugins = [ __import__('electrum_plugins.'+name, fromlist=['electrum_plugins']) for name in plugin_names]
342
343         self.plugin_hooks = {}
344         for p in self.plugins:
345             try:
346                 p.init(self)
347             except:
348                 print_msg("Error:cannot initialize plugin",p)
349                 traceback.print_exc(file=sys.stdout)
350
351     def set_hook(self, name, callback):
352         h = self.plugin_hooks.get(name, [])
353         h.append(callback)
354         self.plugin_hooks[name] = h
355
356     def unset_hook(self, name, callback):
357         h = self.plugin_hooks.get(name,[])
358         if callback in h: h.remove(callback)
359         self.plugin_hooks[name] = h
360
361     def run_hook(self, name, args):
362         for cb in self.plugin_hooks.get(name,[]):
363             apply(cb, args)
364
365
366     # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user
367     def getOpenFileName(self, title, filter = None):
368         directory = self.config.get('io_dir', os.path.expanduser('~'))
369         fileName = unicode( QFileDialog.getOpenFileName(self, title, directory, filter) )
370         if fileName and directory != os.path.dirname(fileName):
371             self.config.set_key('io_dir', os.path.dirname(fileName), True)
372         return fileName
373
374     def getSaveFileName(self, title, filename, filter = None):
375         directory = self.config.get('io_dir', os.path.expanduser('~'))
376         path = os.path.join( directory, filename )
377         fileName = unicode( QFileDialog.getSaveFileName(self, title, path, filter) )
378         if fileName and directory != os.path.dirname(fileName):
379             self.config.set_key('io_dir', os.path.dirname(fileName), True)
380         return fileName
381
382
383
384     def close(self):
385         QMainWindow.close(self)
386         self.run_hook('close_main_window', (self,))
387
388     def connect_slots(self, sender):
389         self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
390         self.previous_payto_e=''
391
392     def timer_actions(self):
393         self.run_hook('timer_actions', (self,))
394             
395         if self.payto_e.hasFocus():
396             return
397         r = unicode( self.payto_e.text() )
398         if r != self.previous_payto_e:
399             self.previous_payto_e = r
400             r = r.strip()
401             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
402                 try:
403                     to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
404                 except:
405                     return
406                 if to_address:
407                     s = r + '  <' + to_address + '>'
408                     self.payto_e.setText(s)
409
410
411
412     def update_status(self):
413         if self.wallet.interface and self.wallet.interface.is_connected:
414             if not self.wallet.up_to_date:
415                 text = _("Synchronizing...")
416                 icon = QIcon(":icons/status_waiting.png")
417             else:
418                 c, u = self.wallet.get_balance()
419                 text =  _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
420                 if u: text +=  "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
421                 text += self.create_quote_text(Decimal(c+u)/100000000)
422                 icon = QIcon(":icons/status_connected.png")
423         else:
424             text = _("Not connected")
425             icon = QIcon(":icons/status_disconnected.png")
426
427         self.status_text = text
428         self.statusBar().showMessage(text)
429         self.status_button.setIcon( icon )
430
431     def update_wallet(self):
432         self.update_status()
433         if self.wallet.up_to_date or not self.wallet.interface.is_connected:
434             self.update_history_tab()
435             self.update_receive_tab()
436             self.update_contacts_tab()
437             self.update_completions()
438
439
440     def create_quote_text(self, btc_balance):
441         quote_currency = self.config.get("currency", "None")
442         quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
443         if quote_balance is None:
444             quote_text = ""
445         else:
446             quote_text = "  (%.2f %s)" % (quote_balance, quote_currency)
447         return quote_text
448         
449     def create_history_tab(self):
450         self.history_list = l = MyTreeWidget(self)
451         l.setColumnCount(5)
452         for i,width in enumerate(self.column_widths['history']):
453             l.setColumnWidth(i, width)
454         l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
455         self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
456         self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
457
458         l.setContextMenuPolicy(Qt.CustomContextMenu)
459         l.customContextMenuRequested.connect(self.create_history_menu)
460         return l
461
462
463     def create_history_menu(self, position):
464         self.history_list.selectedIndexes() 
465         item = self.history_list.currentItem()
466         if not item: return
467         tx_hash = str(item.data(0, Qt.UserRole).toString())
468         if not tx_hash: return
469         menu = QMenu()
470         #menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
471         menu.addAction(_("Details"), lambda: self.show_tx_details(self.wallet.transactions.get(tx_hash)))
472         menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
473         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
474
475
476     def show_tx_details(self, tx):
477         dialog = QDialog(self)
478         dialog.setModal(1)
479         dialog.setWindowTitle(_("Transaction Details"))
480         vbox = QVBoxLayout()
481         dialog.setLayout(vbox)
482         dialog.setMinimumSize(600,300)
483
484         tx_hash = tx.hash()
485         if tx_hash in self.wallet.transactions.keys():
486             is_mine, v, fee = self.wallet.get_tx_value(tx)
487             conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
488             if timestamp:
489                 time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
490             else:
491                 time_str = 'pending'
492         else:
493             is_mine = False
494
495         vbox.addWidget(QLabel("Transaction ID:"))
496         e  = QLineEdit(tx_hash)
497         e.setReadOnly(True)
498         vbox.addWidget(e)
499
500         vbox.addWidget(QLabel("Date: %s"%time_str))
501         vbox.addWidget(QLabel("Status: %d confirmations"%conf))
502         if is_mine:
503             if fee: 
504                 vbox.addWidget(QLabel("Amount sent: %s"% format_satoshis(v-fee, False)))
505                 vbox.addWidget(QLabel("Transaction fee: %s"% format_satoshis(fee, False)))
506             else:
507                 vbox.addWidget(QLabel("Amount sent: %s"% format_satoshis(v, False)))
508                 vbox.addWidget(QLabel("Transaction fee: unknown"))
509         else:
510             vbox.addWidget(QLabel("Amount received: %s"% format_satoshis(v, False)))
511
512         vbox.addWidget( self.generate_transaction_information_widget(tx) )
513
514         ok_button = QPushButton(_("Close"))
515         ok_button.setDefault(True)
516         ok_button.clicked.connect(dialog.accept)
517         
518         hbox = QHBoxLayout()
519         hbox.addStretch(1)
520         hbox.addWidget(ok_button)
521         vbox.addLayout(hbox)
522         dialog.exec_()
523
524     def tx_label_clicked(self, item, column):
525         if column==2 and item.isSelected():
526             self.is_edit=True
527             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
528             self.history_list.editItem( item, column )
529             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
530             self.is_edit=False
531
532     def tx_label_changed(self, item, column):
533         if self.is_edit: 
534             return
535         self.is_edit=True
536         tx_hash = str(item.data(0, Qt.UserRole).toString())
537         tx = self.wallet.transactions.get(tx_hash)
538         s = self.wallet.labels.get(tx_hash)
539         text = unicode( item.text(2) )
540         if text: 
541             self.wallet.labels[tx_hash] = text
542             # Label changed
543             self.run_hook('label_changed',(self, str(tx_hash), text))
544
545             item.setForeground(2, QBrush(QColor('black')))
546         else:
547             if s: self.wallet.labels.pop(tx_hash)
548             text = self.wallet.get_default_label(tx_hash)
549             item.setText(2, text)
550             item.setForeground(2, QBrush(QColor('gray')))
551         self.is_edit=False
552
553
554     def edit_label(self, is_recv):
555         l = self.receive_list if is_recv else self.contacts_list
556         item = l.currentItem()
557         item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
558         l.editItem( item, 1 )
559         item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
560
561
562
563     def address_label_clicked(self, item, column, l, column_addr, column_label):
564         if column == column_label and item.isSelected():
565             addr = unicode( item.text(column_addr) )
566             label = unicode( item.text(column_label) )
567             if label in self.wallet.aliases.keys():
568                 return
569             item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
570             l.editItem( item, column )
571             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
572
573
574     def address_label_changed(self, item, column, l, column_addr, column_label):
575         if column == column_label:
576             addr = unicode( item.text(column_addr) )
577             text = unicode( item.text(column_label) )
578             changed = False
579
580             if text:
581                 if text not in self.wallet.aliases.keys():
582                     old_addr = self.wallet.labels.get(text)
583                     if old_addr != addr:
584                         self.wallet.labels[addr] = text
585                         changed = True
586                         self.run_hook('label_changed',(self, addr, text))
587                 else:
588                     print_error("Error: This is one of your aliases")
589                     label = self.wallet.labels.get(addr,'')
590                     item.setText(column_label, QString(label))
591             else:
592                 s = self.wallet.labels.get(addr)
593                 if s: 
594                     self.wallet.labels.pop(addr)
595                     changed = True
596
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',(self, item, column))
604
605
606     def current_item_changed(self, a):
607         self.run_hook('current_item_changed',(self, 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',(self,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', (self.wallet, self, tx))
814
815         if label: 
816             self.wallet.labels[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', (self, 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                 if x in self.wallet.labels.keys():
1031                     self.wallet.labels.pop(x)
1032             elif is_alias and x in self.wallet.aliases:
1033                 self.wallet.aliases.pop(x)
1034             self.update_history_tab()
1035             self.update_contacts_tab()
1036             self.update_completions()
1037
1038     def create_contact_menu(self, position):
1039         # fixme: this function apparently has a side effect.
1040         # if it is not called the menu pops up several times
1041         #self.contacts_list.selectedIndexes() 
1042
1043         item = self.contacts_list.itemAt(position)
1044         if not item: return
1045         addr = unicode(item.text(0))
1046         label = unicode(item.text(1))
1047         is_alias = label in self.wallet.aliases.keys()
1048         x = label if is_alias else addr
1049         menu = QMenu()
1050         menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1051         menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
1052         menu.addAction(_("QR code"), lambda: self.show_qrcode("bitcoin:" + addr, _("Address")))
1053         if not is_alias:
1054             menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1055         else:
1056             menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
1057         menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
1058         menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1059
1060
1061     def update_receive_item(self, item):
1062         item.setFont(0, QFont(MONOSPACE_FONT))
1063         address = str(item.data(0,0).toString())
1064         label = self.wallet.labels.get(address,'')
1065         item.setData(1,0,label)
1066
1067         self.run_hook('update_receive_item', (self, address, item))
1068                 
1069         c, u = self.wallet.get_addr_balance(address)
1070         balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1071         item.setData(2,0,balance)
1072
1073         if self.expert_mode:
1074             if address in self.wallet.frozen_addresses: 
1075                 item.setBackgroundColor(0, QColor('lightblue'))
1076             elif address in self.wallet.prioritized_addresses: 
1077                 item.setBackgroundColor(0, QColor('lightgreen'))
1078         
1079
1080     def update_receive_tab(self):
1081         l = self.receive_list
1082         
1083         l.clear()
1084         l.setColumnHidden(2, not self.expert_mode)
1085         l.setColumnHidden(3, not self.expert_mode)
1086         if not self.expert_mode:
1087             width = self.column_widths['receive'][0][0]
1088             l.setColumnWidth(0, width)
1089         else:
1090             for i,width in enumerate(self.column_widths['receive'][self.expert_mode]):
1091                 l.setColumnWidth(i, width)        
1092
1093
1094         for k, account in self.wallet.accounts.items():
1095             name = account.get('name',str(k))
1096             c,u = self.wallet.get_account_balance(k)
1097             account_item = QTreeWidgetItem( [ name, '', format_satoshis(c+u), ''] )
1098             l.addTopLevelItem(account_item)
1099             account_item.setExpanded(True)
1100             
1101
1102             for is_change in [0,1]:
1103                 name = "Receiving" if not is_change else "Change"
1104                 seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1105                 account_item.addChild(seq_item)
1106                 if not is_change: seq_item.setExpanded(True)
1107                 is_red = False
1108                 gap = 0
1109
1110                 for address in account[is_change]:
1111                     h = self.wallet.history.get(address,[])
1112             
1113                     if not is_change:
1114                         if h == []:
1115                             gap += 1
1116                             if gap > self.wallet.gap_limit:
1117                                 is_red = True
1118                         else:
1119                             gap = 0
1120
1121                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1122                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1123                     self.update_receive_item(item)
1124                     if is_red:
1125                         item.setBackgroundColor(1, QColor('red'))
1126                     seq_item.addChild(item)
1127
1128         if self.wallet.imported_keys:
1129             c,u = self.wallet.get_imported_balance()
1130             account_item = QTreeWidgetItem( [ _('Imported'), '', format_satoshis(c+u), ''] )
1131             l.addTopLevelItem(account_item)
1132             account_item.setExpanded(True)
1133             for address in self.wallet.imported_keys.keys():
1134                 item = QTreeWidgetItem( [ address, '', '', ''] )
1135                 self.update_receive_item(item)
1136                 account_item.addChild(item)
1137                 
1138
1139         # we use column 1 because column 0 may be hidden
1140         l.setCurrentItem(l.topLevelItem(0),1)
1141
1142     def show_contact_details(self, m):
1143         a = self.wallet.aliases.get(m)
1144         if a:
1145             if a[0] in self.wallet.authorities.keys():
1146                 s = self.wallet.authorities.get(a[0])
1147             else:
1148                 s = "self-signed"
1149             msg = _('Alias:')+' '+ m + '\n'+_('Target address:')+' '+ a[1] + '\n\n'+_('Signed by:')+' ' + s + '\n'+_('Signing address:')+' ' + a[0]
1150             QMessageBox.information(self, 'Alias', msg, 'OK')
1151
1152     def update_contacts_tab(self):
1153
1154         l = self.contacts_list
1155         l.clear()
1156
1157         alias_targets = []
1158         for alias, v in self.wallet.aliases.items():
1159             s, target = v
1160             alias_targets.append(target)
1161             item = QTreeWidgetItem( [ target, alias, '-'] )
1162             item.setBackgroundColor(0, QColor('lightgray'))
1163             l.addTopLevelItem(item)
1164             
1165         for address in self.wallet.addressbook:
1166             if address in alias_targets: continue
1167             label = self.wallet.labels.get(address,'')
1168             n = 0 
1169             for tx in self.wallet.transactions.values():
1170                 if address in map(lambda x: x[0], tx.outputs): n += 1
1171             
1172             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1173             item.setFont(0, QFont(MONOSPACE_FONT))
1174             l.addTopLevelItem(item)
1175
1176         l.setCurrentItem(l.topLevelItem(0))
1177
1178
1179     def create_console_tab(self):
1180         from qt_console import Console
1181         self.console = console = Console()
1182         self.console.history = self.config.get("console-history",[])
1183         self.console.history_index = len(self.console.history)
1184
1185         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1186         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1187
1188         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1189         methods = {}
1190         def mkfunc(f, method):
1191             return lambda *args: apply( f, (method, args, self.password_dialog ))
1192         for m in dir(c):
1193             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1194             methods[m] = mkfunc(c._run, m)
1195             
1196         console.updateNamespace(methods)
1197         return console
1198
1199
1200     def create_status_bar(self):
1201         self.status_text = ""
1202         sb = QStatusBar()
1203         sb.setFixedHeight(35)
1204         qtVersion = qVersion()
1205
1206         update_notification = UpdateLabel(self.config)
1207         if(update_notification.new_version):
1208             sb.addPermanentWidget(update_notification)
1209
1210         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1211             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1212         if self.wallet.seed:
1213             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), _("Password"), lambda: self.change_password_dialog(self.wallet, self) ) )
1214         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1215         if self.wallet.seed:
1216             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) )
1217         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.network_dialog(self.wallet, self) ) 
1218         sb.addPermanentWidget( self.status_button )
1219
1220         self.run_hook('create_status_bar', (sb,))
1221
1222         self.setStatusBar(sb)
1223         
1224     def go_lite(self):
1225         import gui_lite
1226         self.config.set_key('gui', 'lite', True)
1227         self.hide()
1228         if self.lite:
1229             self.lite.mini.show()
1230         else:
1231             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1232             self.lite.main(None)
1233
1234     def new_contact_dialog(self):
1235         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1236         address = unicode(text)
1237         if ok:
1238             if is_valid(address):
1239                 self.wallet.addressbook.append(address)
1240                 self.wallet.save()
1241                 self.update_contacts_tab()
1242                 self.update_history_tab()
1243                 self.update_completions()
1244             else:
1245                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1246
1247     def show_master_public_key(self):
1248         dialog = QDialog(self)
1249         dialog.setModal(1)
1250         dialog.setWindowTitle(_("Master Public Key"))
1251
1252         main_text = QTextEdit()
1253         main_text.setText(self.wallet.get_master_public_key())
1254         main_text.setReadOnly(True)
1255         main_text.setMaximumHeight(170)
1256         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1257
1258         ok_button = QPushButton(_("OK"))
1259         ok_button.setDefault(True)
1260         ok_button.clicked.connect(dialog.accept)
1261
1262         main_layout = QGridLayout()
1263         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1264
1265         main_layout.addWidget(main_text, 1, 0)
1266         main_layout.addWidget(qrw, 1, 1 )
1267
1268         vbox = QVBoxLayout()
1269         vbox.addLayout(main_layout)
1270         hbox = QHBoxLayout()
1271         hbox.addStretch(1)
1272         hbox.addWidget(ok_button)
1273         vbox.addLayout(hbox)
1274
1275         dialog.setLayout(vbox)
1276         dialog.exec_()
1277         
1278
1279     @protected
1280     def show_seed_dialog(self, password):
1281         if not self.wallet.seed:
1282             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1283             return
1284         try:
1285             seed = self.wallet.decode_seed(password)
1286         except:
1287             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1288             return
1289         self.show_seed(seed, self)
1290
1291
1292     @classmethod
1293     def show_seed(self, seed, parent=None):
1294         dialog = QDialog(parent)
1295         dialog.setModal(1)
1296         dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
1297
1298         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1299
1300         label1 = QLabel(_("Your wallet generation seed is")+ ":")
1301
1302         seed_text = QTextEdit(brainwallet)
1303         seed_text.setReadOnly(True)
1304         seed_text.setMaximumHeight(130)
1305         
1306         msg2 =  _("Please write down or memorize these 12 words (order is important).") + " " \
1307               + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
1308               + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
1309               + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
1310         label2 = QLabel(msg2)
1311         label2.setWordWrap(True)
1312
1313         logo = QLabel()
1314         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1315         logo.setMaximumWidth(60)
1316
1317         qrw = QRCodeWidget(seed)
1318
1319         ok_button = QPushButton(_("OK"))
1320         ok_button.setDefault(True)
1321         ok_button.clicked.connect(dialog.accept)
1322
1323         grid = QGridLayout()
1324         #main_layout.addWidget(logo, 0, 0)
1325
1326         grid.addWidget(logo, 0, 0)
1327         grid.addWidget(label1, 0, 1)
1328
1329         grid.addWidget(seed_text, 1, 0, 1, 2)
1330
1331         grid.addWidget(qrw, 0, 2, 2, 1)
1332
1333         vbox = QVBoxLayout()
1334         vbox.addLayout(grid)
1335         vbox.addWidget(label2)
1336
1337         hbox = QHBoxLayout()
1338         hbox.addStretch(1)
1339         hbox.addWidget(ok_button)
1340         vbox.addLayout(hbox)
1341
1342         dialog.setLayout(vbox)
1343         dialog.exec_()
1344
1345     def show_qrcode(self, data, title = "QR code"):
1346         if not data: return
1347         d = QDialog(self)
1348         d.setModal(1)
1349         d.setWindowTitle(title)
1350         d.setMinimumSize(270, 300)
1351         vbox = QVBoxLayout()
1352         qrw = QRCodeWidget(data)
1353         vbox.addWidget(qrw, 1)
1354         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1355         hbox = QHBoxLayout()
1356         hbox.addStretch(1)
1357
1358         def print_qr(self):
1359             filename = "qrcode.bmp"
1360             bmp.save_qrcode(qrw.qr, filename)
1361             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1362
1363         b = QPushButton(_("Save"))
1364         hbox.addWidget(b)
1365         b.clicked.connect(print_qr)
1366
1367         b = QPushButton(_("Close"))
1368         hbox.addWidget(b)
1369         b.clicked.connect(d.accept)
1370         b.setDefault(True)
1371
1372         vbox.addLayout(hbox)
1373         d.setLayout(vbox)
1374         d.exec_()
1375
1376
1377     def do_protect(self, func, args):
1378         if self.wallet.use_encryption:
1379             password = self.password_dialog()
1380             if not password:
1381                 return
1382         else:
1383             password = None
1384             
1385         if args != (False,):
1386             args = (self,) + args + (password,)
1387         else:
1388             args = (self,password)
1389         apply( func, args)
1390
1391
1392     @protected
1393     def show_private_key(self, address, password):
1394         if not address: return
1395         try:
1396             pk = self.wallet.get_private_key(address, password)
1397         except BaseException, e:
1398             self.show_message(str(e))
1399             return
1400         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1401
1402
1403     @protected
1404     def do_sign(self, address, message, signature, password):
1405         try:
1406             sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1407             signature.setText(sig)
1408         except BaseException, e:
1409             self.show_message(str(e))
1410
1411     def sign_message(self, address):
1412         if not address: return
1413         d = QDialog(self)
1414         d.setModal(1)
1415         d.setWindowTitle(_('Sign Message'))
1416         d.setMinimumSize(410, 290)
1417
1418         tab_widget = QTabWidget()
1419         tab = QWidget()
1420         layout = QGridLayout(tab)
1421
1422         sign_address = QLineEdit()
1423
1424         sign_address.setText(address)
1425         layout.addWidget(QLabel(_('Address')), 1, 0)
1426         layout.addWidget(sign_address, 1, 1)
1427
1428         sign_message = QTextEdit()
1429         layout.addWidget(QLabel(_('Message')), 2, 0)
1430         layout.addWidget(sign_message, 2, 1)
1431         layout.setRowStretch(2,3)
1432
1433         sign_signature = QTextEdit()
1434         layout.addWidget(QLabel(_('Signature')), 3, 0)
1435         layout.addWidget(sign_signature, 3, 1)
1436         layout.setRowStretch(3,1)
1437
1438
1439         hbox = QHBoxLayout()
1440         b = QPushButton(_("Sign"))
1441         hbox.addWidget(b)
1442         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1443         b = QPushButton(_("Close"))
1444         b.clicked.connect(d.accept)
1445         hbox.addWidget(b)
1446         layout.addLayout(hbox, 4, 1)
1447         tab_widget.addTab(tab, _("Sign"))
1448
1449
1450         tab = QWidget()
1451         layout = QGridLayout(tab)
1452
1453         verify_address = QLineEdit()
1454         layout.addWidget(QLabel(_('Address')), 1, 0)
1455         layout.addWidget(verify_address, 1, 1)
1456
1457         verify_message = QTextEdit()
1458         layout.addWidget(QLabel(_('Message')), 2, 0)
1459         layout.addWidget(verify_message, 2, 1)
1460         layout.setRowStretch(2,3)
1461
1462         verify_signature = QTextEdit()
1463         layout.addWidget(QLabel(_('Signature')), 3, 0)
1464         layout.addWidget(verify_signature, 3, 1)
1465         layout.setRowStretch(3,1)
1466
1467         def do_verify():
1468             try:
1469                 self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
1470                 self.show_message(_("Signature verified"))
1471             except BaseException, e:
1472                 self.show_message(str(e))
1473                 return
1474
1475         hbox = QHBoxLayout()
1476         b = QPushButton(_("Verify"))
1477         b.clicked.connect(do_verify)
1478         hbox.addWidget(b)
1479         b = QPushButton(_("Close"))
1480         b.clicked.connect(d.accept)
1481         hbox.addWidget(b)
1482         layout.addLayout(hbox, 4, 1)
1483         tab_widget.addTab(tab, _("Verify"))
1484
1485         vbox = QVBoxLayout()
1486         vbox.addWidget(tab_widget)
1487         d.setLayout(vbox)
1488         d.exec_()
1489
1490         
1491
1492
1493     def question(self, msg):
1494         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1495
1496     def show_message(self, msg):
1497         QMessageBox.information(self, _('Message'), msg, _('OK'))
1498
1499     def password_dialog(self ):
1500         d = QDialog(self)
1501         d.setModal(1)
1502
1503         pw = QLineEdit()
1504         pw.setEchoMode(2)
1505
1506         vbox = QVBoxLayout()
1507         msg = _('Please enter your password')
1508         vbox.addWidget(QLabel(msg))
1509
1510         grid = QGridLayout()
1511         grid.setSpacing(8)
1512         grid.addWidget(QLabel(_('Password')), 1, 0)
1513         grid.addWidget(pw, 1, 1)
1514         vbox.addLayout(grid)
1515
1516         vbox.addLayout(ok_cancel_buttons(d))
1517         d.setLayout(vbox) 
1518
1519         if not d.exec_(): return
1520         return unicode(pw.text())
1521
1522
1523
1524
1525
1526     @staticmethod
1527     def change_password_dialog( wallet, parent=None ):
1528
1529         if not wallet.seed:
1530             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1531             return
1532
1533         d = QDialog(parent)
1534         d.setModal(1)
1535
1536         pw = QLineEdit()
1537         pw.setEchoMode(2)
1538         new_pw = QLineEdit()
1539         new_pw.setEchoMode(2)
1540         conf_pw = QLineEdit()
1541         conf_pw.setEchoMode(2)
1542
1543         vbox = QVBoxLayout()
1544         if parent:
1545             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1546                    +_('To disable wallet encryption, enter an empty new password.')) \
1547                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1548         else:
1549             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1550                   +_("Leave these fields empty if you want to disable encryption.")
1551         vbox.addWidget(QLabel(msg))
1552
1553         grid = QGridLayout()
1554         grid.setSpacing(8)
1555
1556         if wallet.use_encryption:
1557             grid.addWidget(QLabel(_('Password')), 1, 0)
1558             grid.addWidget(pw, 1, 1)
1559
1560         grid.addWidget(QLabel(_('New Password')), 2, 0)
1561         grid.addWidget(new_pw, 2, 1)
1562
1563         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1564         grid.addWidget(conf_pw, 3, 1)
1565         vbox.addLayout(grid)
1566
1567         vbox.addLayout(ok_cancel_buttons(d))
1568         d.setLayout(vbox) 
1569
1570         if not d.exec_(): return
1571
1572         password = unicode(pw.text()) if wallet.use_encryption else None
1573         new_password = unicode(new_pw.text())
1574         new_password2 = unicode(conf_pw.text())
1575
1576         try:
1577             seed = wallet.decode_seed(password)
1578         except:
1579             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1580             return
1581
1582         if new_password != new_password2:
1583             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1584             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1585
1586         wallet.update_password(seed, password, new_password)
1587
1588     @staticmethod
1589     def seed_dialog(wallet, parent=None):
1590         d = QDialog(parent)
1591         d.setModal(1)
1592
1593         vbox = QVBoxLayout()
1594         msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1595         vbox.addWidget(QLabel(msg))
1596
1597         grid = QGridLayout()
1598         grid.setSpacing(8)
1599
1600         seed_e = QLineEdit()
1601         grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1602         grid.addWidget(seed_e, 1, 1)
1603
1604         gap_e = QLineEdit()
1605         gap_e.setText("5")
1606         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1607         grid.addWidget(gap_e, 2, 1)
1608         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1609         vbox.addLayout(grid)
1610
1611         vbox.addLayout(ok_cancel_buttons(d))
1612         d.setLayout(vbox) 
1613
1614         if not d.exec_(): return
1615
1616         try:
1617             gap = int(unicode(gap_e.text()))
1618         except:
1619             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1620             return
1621
1622         try:
1623             seed = str(seed_e.text())
1624             seed.decode('hex')
1625         except:
1626             print_error("Warning: Not hex, trying decode")
1627             try:
1628                 seed = mnemonic.mn_decode( seed.split(' ') )
1629             except:
1630                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1631                 return
1632
1633         if not seed:
1634             QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
1635             return
1636
1637         return seed, gap
1638
1639     def generate_transaction_information_widget(self, tx):
1640         tabs = QTabWidget(self)
1641
1642         tab1 = QWidget()
1643         grid_ui = QGridLayout(tab1)
1644         grid_ui.setColumnStretch(0,1)
1645         tabs.addTab(tab1, _('Outputs') )
1646
1647         tree_widget = MyTreeWidget(self)
1648         tree_widget.setColumnCount(2)
1649         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1650         tree_widget.setColumnWidth(0, 300)
1651         tree_widget.setColumnWidth(1, 50)
1652
1653         for address, value in tx.outputs:
1654             item = QTreeWidgetItem( [address, "%s" % ( format_satoshis(value))] )
1655             tree_widget.addTopLevelItem(item)
1656
1657         tree_widget.setMaximumHeight(100)
1658
1659         grid_ui.addWidget(tree_widget)
1660
1661         tab2 = QWidget()
1662         grid_ui = QGridLayout(tab2)
1663         grid_ui.setColumnStretch(0,1)
1664         tabs.addTab(tab2, _('Inputs') )
1665         
1666         tree_widget = MyTreeWidget(self)
1667         tree_widget.setColumnCount(2)
1668         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1669
1670         for input_line in tx.inputs:
1671             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1672             tree_widget.addTopLevelItem(item)
1673
1674         tree_widget.setMaximumHeight(100)
1675
1676         grid_ui.addWidget(tree_widget)
1677         return tabs
1678
1679
1680     def tx_dict_from_text(self, txt):
1681         try:
1682             tx_dict = json.loads(str(txt))
1683             assert "hex" in tx_dict.keys()
1684             assert "complete" in tx_dict.keys()
1685             if not tx_dict["complete"]:
1686                 assert "input_info" in tx_dict.keys()
1687         except:
1688             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1689             return None
1690         return tx_dict
1691
1692
1693     def read_tx_from_file(self):
1694         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1695         if not fileName:
1696             return
1697         try:
1698             with open(fileName, "r") as f:
1699                 file_content = f.read()
1700         except (ValueError, IOError, os.error), reason:
1701             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1702
1703         return self.tx_dict_from_text(file_content)
1704
1705
1706     @protected
1707     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
1708         try:
1709             self.wallet.signrawtransaction(tx, input_info, [], password)
1710             
1711             fileName = self.getSaveFileName(_("Select where to save your signed transaction"), 'signed_%s.txn' % (tx.hash()[0:8]), "*.txn")
1712             if fileName:
1713                 with open(fileName, "w+") as f:
1714                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1715                 self.show_message(_("Transaction saved successfully"))
1716                 if dialog:
1717                     dialog.done(0)
1718         except BaseException, e:
1719             self.show_message(str(e))
1720     
1721
1722     def send_raw_transaction(self, raw_tx, dialog = ""):
1723         result, result_message = self.wallet.sendtx( raw_tx )
1724         if result:
1725             self.show_message("Transaction successfully sent: %s" % (result_message))
1726             if dialog:
1727                 dialog.done(0)
1728         else:
1729             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1730
1731     def do_process_from_text(self):
1732         dialog = QDialog(self)
1733         dialog.setMinimumWidth(500)
1734         dialog.setWindowTitle(_('Input raw transaction'))
1735         dialog.setModal(1)
1736         l = QVBoxLayout()
1737         dialog.setLayout(l)
1738         l.addWidget(QLabel(_("Transaction:")))
1739         txt = QTextEdit()
1740         l.addWidget(txt)
1741
1742         ok_button = QPushButton(_("Load transaction"))
1743         ok_button.setDefault(True)
1744         ok_button.clicked.connect(dialog.accept)
1745         l.addWidget(ok_button)
1746
1747         dialog.exec_()
1748         tx_dict = self.tx_dict_from_text(unicode(txt.toPlainText()))
1749         if tx_dict:
1750             self.create_process_transaction_window(tx_dict)
1751
1752     def do_process_from_file(self):
1753         tx_dict = self.read_tx_from_file()
1754         if tx_dict: 
1755             self.create_process_transaction_window(tx_dict)
1756
1757     def create_process_transaction_window(self, tx_dict):
1758         tx = Transaction(tx_dict["hex"])
1759             
1760         dialog = QDialog(self)
1761         dialog.setMinimumWidth(500)
1762         dialog.setWindowTitle(_('Process raw transaction'))
1763         dialog.setModal(1)
1764
1765         l = QGridLayout()
1766         dialog.setLayout(l)
1767
1768         l.addWidget(QLabel(_("Transaction status:")), 3,0)
1769         l.addWidget(QLabel(_("Actions")), 4,0)
1770
1771         if tx_dict["complete"] == False:
1772             l.addWidget(QLabel(_("Unsigned")), 3,1)
1773             if self.wallet.seed :
1774                 b = QPushButton("Sign transaction")
1775                 input_info = json.loads(tx_dict["input_info"])
1776                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
1777                 l.addWidget(b, 4, 1)
1778             else:
1779                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
1780         else:
1781             l.addWidget(QLabel(_("Signed")), 3,1)
1782             b = QPushButton("Broadcast transaction")
1783             b.clicked.connect(lambda: self.send_raw_transaction(tx, dialog))
1784             l.addWidget(b,4,1)
1785
1786         l.addWidget( self.generate_transaction_information_widget(tx), 0,0,2,3)
1787         cancelButton = QPushButton(_("Cancel"))
1788         cancelButton.clicked.connect(lambda: dialog.done(0))
1789         l.addWidget(cancelButton, 4,2)
1790
1791         dialog.exec_()
1792
1793
1794     @protected
1795     def do_export_privkeys(self, password):
1796         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.")))
1797
1798         try:
1799             select_export = _('Select file to export your private keys to')
1800             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1801             if fileName:
1802                 with open(fileName, "w+") as csvfile:
1803                     transaction = csv.writer(csvfile)
1804                     transaction.writerow(["address", "private_key"])
1805
1806                     
1807                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1808                         transaction.writerow(["%34s"%addr,pk])
1809
1810                     self.show_message(_("Private keys exported."))
1811
1812         except (IOError, os.error), reason:
1813             export_error_label = _("Electrum was unable to produce a private key-export.")
1814             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1815
1816         except BaseException, e:
1817           self.show_message(str(e))
1818           return
1819
1820
1821     def do_import_labels(self):
1822         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1823         if not labelsFile: return
1824         try:
1825             f = open(labelsFile, 'r')
1826             data = f.read()
1827             f.close()
1828             for key, value in json.loads(data).items():
1829                 self.wallet.labels[key] = value
1830             self.wallet.save()
1831             QMessageBox.information(None, _("Labels imported"), _("Your labels where imported from")+" '%s'" % str(labelsFile))
1832         except (IOError, os.error), reason:
1833             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1834             
1835
1836     def do_export_labels(self):
1837         labels = self.wallet.labels
1838         try:
1839             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1840             if fileName:
1841                 with open(fileName, 'w+') as f:
1842                     json.dump(labels, f)
1843                 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1844         except (IOError, os.error), reason:
1845             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1846
1847
1848     def do_export_history(self):
1849         from gui_lite import csv_transaction
1850         csv_transaction(self.wallet)
1851
1852
1853     @protected
1854     def do_import_privkey(self, password):
1855         if not self.wallet.imported_keys:
1856             r = QMessageBox.question(None, _('Warning'), _('Warning: Imported keys are not recoverable from seed.') + ' ' \
1857                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '\n\n' \
1858                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1859             if r == 4: return
1860
1861         text, ok = QInputDialog.getText(self, _('Import private key'), _('Private Key') + ':')
1862         if not ok: return
1863         sec = str(text).strip()
1864         try:
1865             addr = self.wallet.import_key(sec, password)
1866             if not addr:
1867                 QMessageBox.critical(None, _("Unable to import key"), "error")
1868             else:
1869                 QMessageBox.information(None, _("Key imported"), addr)
1870                 self.update_receive_tab()
1871                 self.update_history_tab()
1872         except BaseException as e:
1873             QMessageBox.critical(None, _("Unable to import key"), str(e))
1874
1875
1876     def settings_dialog(self):
1877         d = QDialog(self)
1878         d.setWindowTitle(_('Electrum Settings'))
1879         d.setModal(1)
1880         vbox = QVBoxLayout()
1881
1882         tabs = QTabWidget(self)
1883         self.settings_tab = tabs
1884         vbox.addWidget(tabs)
1885
1886         tab1 = QWidget()
1887         grid_ui = QGridLayout(tab1)
1888         grid_ui.setColumnStretch(0,1)
1889         tabs.addTab(tab1, _('Display') )
1890
1891         nz_label = QLabel(_('Display zeros'))
1892         grid_ui.addWidget(nz_label, 0, 0)
1893         nz_e = QLineEdit()
1894         nz_e.setText("%d"% self.wallet.num_zeros)
1895         grid_ui.addWidget(nz_e, 0, 1)
1896         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1897         grid_ui.addWidget(HelpButton(msg), 0, 2)
1898         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1899         if not self.config.is_modifiable('num_zeros'):
1900             for w in [nz_e, nz_label]: w.setEnabled(False)
1901         
1902         lang_label=QLabel(_('Language') + ':')
1903         grid_ui.addWidget(lang_label, 1, 0)
1904         lang_combo = QComboBox()
1905         from i18n import languages
1906         lang_combo.addItems(languages.values())
1907         try:
1908             index = languages.keys().index(self.config.get("language",''))
1909         except:
1910             index = 0
1911         lang_combo.setCurrentIndex(index)
1912         grid_ui.addWidget(lang_combo, 1, 1)
1913         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1914         if not self.config.is_modifiable('language'):
1915             for w in [lang_combo, lang_label]: w.setEnabled(False)
1916
1917         currencies = self.exchanger.get_currencies()
1918         currencies.insert(0, "None")
1919
1920         cur_label=QLabel(_('Currency') + ':')
1921         grid_ui.addWidget(cur_label , 2, 0)
1922         cur_combo = QComboBox()
1923         cur_combo.addItems(currencies)
1924         try:
1925             index = currencies.index(self.config.get('currency', "None"))
1926         except:
1927             index = 0
1928         cur_combo.setCurrentIndex(index)
1929         grid_ui.addWidget(cur_combo, 2, 1)
1930         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1931         
1932         view_label=QLabel(_('Receive Tab') + ':')
1933         grid_ui.addWidget(view_label , 3, 0)
1934         view_combo = QComboBox()
1935         view_combo.addItems([_('Simple'), _('Advanced')])
1936         view_combo.setCurrentIndex(self.expert_mode)
1937         grid_ui.addWidget(view_combo, 3, 1)
1938         hh = _('This selects the interaction mode of the "Receive" tab.')+' ' + '\n\n' \
1939              + _('Simple') +   ': ' + _('Show only addresses and labels.') + '\n\n' \
1940              + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' 
1941         
1942         grid_ui.addWidget(HelpButton(hh), 3, 2)
1943         grid_ui.setRowStretch(4,1)
1944
1945         # wallet tab
1946         tab2 = QWidget()
1947         grid_wallet = QGridLayout(tab2)
1948         grid_wallet.setColumnStretch(0,1)
1949         tabs.addTab(tab2, _('Wallet') )
1950         
1951         fee_label = QLabel(_('Transaction fee'))
1952         grid_wallet.addWidget(fee_label, 0, 0)
1953         fee_e = QLineEdit()
1954         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1955         grid_wallet.addWidget(fee_e, 0, 2)
1956         msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1957             + _('Recommended value') + ': 0.001'
1958         grid_wallet.addWidget(HelpButton(msg), 0, 3)
1959         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1960         if not self.config.is_modifiable('fee'):
1961             for w in [fee_e, fee_label]: w.setEnabled(False)
1962
1963         usechange_label = QLabel(_('Use change addresses'))
1964         grid_wallet.addWidget(usechange_label, 1, 0)
1965         usechange_combo = QComboBox()
1966         usechange_combo.addItems([_('Yes'), _('No')])
1967         usechange_combo.setCurrentIndex(not self.wallet.use_change)
1968         grid_wallet.addWidget(usechange_combo, 1, 2)
1969         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1970         if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1971
1972         gap_label = QLabel(_('Gap limit'))
1973         grid_wallet.addWidget(gap_label, 2, 0)
1974         gap_e = QLineEdit()
1975         gap_e.setText("%d"% self.wallet.gap_limit)
1976         grid_wallet.addWidget(gap_e, 2, 2)
1977         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1978               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1979               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1980               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1981               + _('Warning') + ': ' \
1982               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1983               + _('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' 
1984         grid_wallet.addWidget(HelpButton(msg), 2, 3)
1985         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1986         if not self.config.is_modifiable('gap_limit'):
1987             for w in [gap_e, gap_label]: w.setEnabled(False)
1988
1989         grid_wallet.setRowStretch(3,1)
1990
1991
1992         # import/export tab
1993         tab3 = QWidget()
1994         grid_io = QGridLayout(tab3)
1995         grid_io.setColumnStretch(0,1)
1996         tabs.addTab(tab3, _('Import/Export') )
1997         
1998         grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1999         grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
2000         grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
2001         grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
2002
2003         grid_io.addWidget(QLabel(_('History')), 2, 0)
2004         grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
2005         grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
2006
2007         grid_io.addWidget(QLabel(_('Private keys')), 3, 0)
2008
2009         grid_io.addWidget(EnterButton(_("Export"), self.do_export_privkeys), 3, 1)
2010         grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
2011         grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
2012
2013         grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
2014         grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
2015         grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
2016                               + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
2017                               + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
2018
2019
2020         grid_io.addWidget(QLabel(_("Load transaction")), 5, 0)
2021         grid_io.addWidget(EnterButton(_("From file"), self.do_process_from_file), 5, 1)
2022         grid_io.addWidget(EnterButton(_("From text"), self.do_process_from_text), 5, 2)
2023         grid_io.addWidget(HelpButton(_("This will give you the option to sign or broadcast a transaction based on it's status.")), 5, 3)
2024
2025         grid_io.setRowStretch(5,1)
2026
2027
2028         # plugins
2029         if self.plugins:
2030             tab5 = QScrollArea()
2031             grid_plugins = QGridLayout(tab5)
2032             grid_plugins.setColumnStretch(0,1)
2033             tabs.addTab(tab5, _('Plugins') )
2034             def mk_toggle(cb, p):
2035                 return lambda: cb.setChecked(p.toggle(self))
2036             for i, p in enumerate(self.plugins):
2037                 try:
2038                     name, description = p.get_info()
2039                     cb = QCheckBox(name)
2040                     cb.setDisabled(not p.is_available())
2041                     cb.setChecked(p.is_enabled())
2042                     cb.clicked.connect(mk_toggle(cb,p))
2043                     grid_plugins.addWidget(cb, i, 0)
2044                     grid_plugins.addWidget(HelpButton(description), i, 2)
2045                 except:
2046                     print_msg("Error: cannot display plugin", p)
2047                     traceback.print_exc(file=sys.stdout)
2048             grid_plugins.setRowStretch(i+1,1)
2049
2050         self.run_hook('create_settings_tab', (self,tabs,))
2051
2052         vbox.addLayout(ok_cancel_buttons(d))
2053         d.setLayout(vbox) 
2054
2055         # run the dialog
2056         if not d.exec_(): return
2057
2058         fee = unicode(fee_e.text())
2059         try:
2060             fee = int( 100000000 * Decimal(fee) )
2061         except:
2062             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2063             return
2064
2065         if self.wallet.fee != fee:
2066             self.wallet.fee = fee
2067             self.wallet.save()
2068         
2069         nz = unicode(nz_e.text())
2070         try:
2071             nz = int( nz )
2072             if nz>8: nz=8
2073         except:
2074             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2075             return
2076
2077         if self.wallet.num_zeros != nz:
2078             self.wallet.num_zeros = nz
2079             self.config.set_key('num_zeros', nz, True)
2080             self.update_history_tab()
2081             self.update_receive_tab()
2082
2083         usechange_result = usechange_combo.currentIndex() == 0
2084         if self.wallet.use_change != usechange_result:
2085             self.wallet.use_change = usechange_result
2086             self.config.set_key('use_change', self.wallet.use_change, True)
2087         
2088         try:
2089             n = int(gap_e.text())
2090         except:
2091             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2092             return
2093
2094         if self.wallet.gap_limit != n:
2095             r = self.wallet.change_gap_limit(n)
2096             if r:
2097                 self.update_receive_tab()
2098                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2099             else:
2100                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2101
2102         need_restart = False
2103
2104         lang_request = languages.keys()[lang_combo.currentIndex()]
2105         if lang_request != self.config.get('language'):
2106             self.config.set_key("language", lang_request, True)
2107             need_restart = True
2108             
2109         cur_request = str(currencies[cur_combo.currentIndex()])
2110         if cur_request != self.config.get('currency', "None"):
2111             self.config.set_key('currency', cur_request, True)
2112             self.update_wallet()
2113
2114         if need_restart:
2115             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2116
2117         self.receive_tab_set_mode(view_combo.currentIndex())
2118
2119
2120     @staticmethod 
2121     def network_dialog(wallet, parent=None):
2122         interface = wallet.interface
2123         if parent:
2124             if interface.is_connected:
2125                 status = _("Connected to")+" %s\n%d "%(interface.host, wallet.verifier.height)+_("blocks")
2126             else:
2127                 status = _("Not connected")
2128             server = interface.server
2129         else:
2130             import random
2131             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
2132             server = interface.server
2133
2134         plist, servers_list = interface.get_servers_list()
2135
2136         d = QDialog(parent)
2137         d.setModal(1)
2138         d.setWindowTitle(_('Server'))
2139         d.setMinimumSize(375, 20)
2140
2141         vbox = QVBoxLayout()
2142         vbox.setSpacing(30)
2143
2144         hbox = QHBoxLayout()
2145         l = QLabel()
2146         l.setPixmap(QPixmap(":icons/network.png"))
2147         hbox.addStretch(10)
2148         hbox.addWidget(l)
2149         hbox.addWidget(QLabel(status))
2150         hbox.addStretch(50)
2151         vbox.addLayout(hbox)
2152
2153
2154         # grid layout
2155         grid = QGridLayout()
2156         grid.setSpacing(8)
2157         vbox.addLayout(grid)
2158
2159         # server
2160         server_protocol = QComboBox()
2161         server_host = QLineEdit()
2162         server_host.setFixedWidth(200)
2163         server_port = QLineEdit()
2164         server_port.setFixedWidth(60)
2165
2166         protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
2167         protocol_letters = 'thsg'
2168         DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
2169         server_protocol.addItems(protocol_names)
2170
2171         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
2172         grid.addWidget(server_protocol, 0, 1)
2173         grid.addWidget(server_host, 0, 2)
2174         grid.addWidget(server_port, 0, 3)
2175
2176         def change_protocol(p):
2177             protocol = protocol_letters[p]
2178             host = unicode(server_host.text())
2179             pp = plist.get(host,DEFAULT_PORTS)
2180             if protocol not in pp.keys():
2181                 protocol = pp.keys()[0]
2182             port = pp[protocol]
2183             server_host.setText( host )
2184             server_port.setText( port )
2185
2186         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
2187         
2188         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
2189         servers_list_widget = QTreeWidget(parent)
2190         servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
2191         servers_list_widget.setMaximumHeight(150)
2192         servers_list_widget.setColumnWidth(0, 240)
2193         for _host in servers_list.keys():
2194             _type = 'P' if servers_list[_host].get('pruning') else 'F'
2195             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
2196
2197         def change_server(host, protocol=None):
2198             pp = plist.get(host,DEFAULT_PORTS)
2199             if protocol:
2200                 port = pp.get(protocol)
2201                 if not port: protocol = None
2202                     
2203             if not protocol:
2204                 if 't' in pp.keys():
2205                     protocol = 't'
2206                     port = pp.get(protocol)
2207                 else:
2208                     protocol = pp.keys()[0]
2209                     port = pp.get(protocol)
2210             
2211             server_host.setText( host )
2212             server_port.setText( port )
2213             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
2214
2215             if not plist: return
2216             for p in protocol_letters:
2217                 i = protocol_letters.index(p)
2218                 j = server_protocol.model().index(i,0)
2219                 if p not in pp.keys():
2220                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
2221                 else:
2222                     server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
2223
2224
2225         if server:
2226             host, port, protocol = server.split(':')
2227             change_server(host,protocol)
2228
2229         servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
2230         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
2231
2232         if not wallet.config.is_modifiable('server'):
2233             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
2234
2235         # auto cycle
2236         autocycle_cb = QCheckBox(_('Try random servers if disconnected'))
2237         autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
2238         grid.addWidget(autocycle_cb, 3, 1, 3, 2)
2239         if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
2240
2241         # proxy setting
2242         proxy_mode = QComboBox()
2243         proxy_host = QLineEdit()
2244         proxy_host.setFixedWidth(200)
2245         proxy_port = QLineEdit()
2246         proxy_port.setFixedWidth(60)
2247         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
2248
2249         def check_for_disable(index = False):
2250             if proxy_mode.currentText() != 'NONE':
2251                 proxy_host.setEnabled(True)
2252                 proxy_port.setEnabled(True)
2253             else:
2254                 proxy_host.setEnabled(False)
2255                 proxy_port.setEnabled(False)
2256
2257         check_for_disable()
2258         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
2259
2260         if not wallet.config.is_modifiable('proxy'):
2261             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
2262
2263         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
2264         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
2265         proxy_host.setText(proxy_config.get("host"))
2266         proxy_port.setText(proxy_config.get("port"))
2267
2268         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
2269         grid.addWidget(proxy_mode, 2, 1)
2270         grid.addWidget(proxy_host, 2, 2)
2271         grid.addWidget(proxy_port, 2, 3)
2272
2273         # buttons
2274         vbox.addLayout(ok_cancel_buttons(d))
2275         d.setLayout(vbox) 
2276
2277         if not d.exec_(): return
2278
2279         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
2280         if proxy_mode.currentText() != 'NONE':
2281             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
2282         else:
2283             proxy = None
2284
2285         wallet.config.set_key("proxy", proxy, True)
2286         wallet.config.set_key("server", server, True)
2287         interface.set_server(server, proxy)
2288         wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
2289         return True
2290
2291     def closeEvent(self, event):
2292         g = self.geometry()
2293         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2294         self.save_column_widths()
2295         self.config.set_key("column-widths", self.column_widths, True)
2296         self.config.set_key("console-history",self.console.history[-50:])
2297         event.accept()
2298
2299
2300 class ElectrumGui:
2301
2302     def __init__(self, wallet, config, app=None):
2303         self.wallet = wallet
2304         self.config = config
2305         if app is None:
2306             self.app = QApplication(sys.argv)
2307
2308
2309     def restore_or_create(self):
2310         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2311         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2312         if r==2: return None
2313         return 'restore' if r==1 else 'create'
2314
2315     def seed_dialog(self):
2316         return ElectrumWindow.seed_dialog( self.wallet )
2317
2318     def network_dialog(self):
2319         return ElectrumWindow.network_dialog( self.wallet, parent=None )
2320         
2321
2322     def show_seed(self):
2323         ElectrumWindow.show_seed(self.wallet.seed)
2324
2325
2326     def password_dialog(self):
2327         if self.wallet.seed:
2328             ElectrumWindow.change_password_dialog(self.wallet)
2329
2330
2331     def restore_wallet(self):
2332         wallet = self.wallet
2333         # wait until we are connected, because the user might have selected another server
2334         if not wallet.interface.is_connected:
2335             waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
2336             waiting_dialog(waiting)
2337
2338         waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
2339             %(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
2340
2341         wallet.set_up_to_date(False)
2342         wallet.interface.poke('synchronizer')
2343         waiting_dialog(waiting)
2344         if wallet.is_found():
2345             print_error( "Recovery successful" )
2346         else:
2347             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2348
2349         return True
2350
2351     def main(self,url):
2352         s = Timer()
2353         s.start()
2354         w = ElectrumWindow(self.wallet, self.config)
2355         if url: w.set_url(url)
2356         w.app = self.app
2357         w.connect_slots(s)
2358         w.update_wallet()
2359         w.show()
2360
2361         self.app.exec_()
2362
2363