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