3ff76f0a308bf071f69034e74cd9c0bcb46896db
[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         for k, account in account_items:
1104             name = account.get('name',str(k))
1105             c,u = self.wallet.get_account_balance(k)
1106             account_item = QTreeWidgetItem( [ name, '', format_satoshis(c+u), ''] )
1107             l.addTopLevelItem(account_item)
1108             account_item.setExpanded(True)
1109             
1110             for is_change in ([0,1] if self.expert_mode else [0]):
1111                 if self.expert_mode:
1112                     name = "Receiving" if not is_change else "Change"
1113                     seq_item = QTreeWidgetItem( [ name, '', '', '', ''] )
1114                     account_item.addChild(seq_item)
1115                     if not is_change: seq_item.setExpanded(True)
1116                 else:
1117                     seq_item = account_item
1118                 is_red = False
1119                 gap = 0
1120
1121                 for address in account[is_change]:
1122                     h = self.wallet.history.get(address,[])
1123             
1124                     if h == []:
1125                         gap += 1
1126                         if gap > self.wallet.gap_limit:
1127                             is_red = True
1128                     else:
1129                         gap = 0
1130
1131                     num_tx = '*' if h == ['*'] else "%d"%len(h)
1132                     item = QTreeWidgetItem( [ address, '', '', num_tx] )
1133                     self.update_receive_item(item)
1134                     if is_red:
1135                         item.setBackgroundColor(1, QColor('red'))
1136                     seq_item.addChild(item)
1137
1138
1139         if self.wallet.imported_keys and (self.current_account is None or self.current_account == -1):
1140             c,u = self.wallet.get_imported_balance()
1141             account_item = QTreeWidgetItem( [ _('Imported'), '', format_satoshis(c+u), ''] )
1142             l.addTopLevelItem(account_item)
1143             account_item.setExpanded(True)
1144             for address in self.wallet.imported_keys.keys():
1145                 item = QTreeWidgetItem( [ address, '', '', ''] )
1146                 self.update_receive_item(item)
1147                 account_item.addChild(item)
1148                 
1149
1150         # we use column 1 because column 0 may be hidden
1151         l.setCurrentItem(l.topLevelItem(0),1)
1152
1153
1154     def update_contacts_tab(self):
1155
1156         l = self.contacts_list
1157         l.clear()
1158
1159         for address in self.wallet.addressbook:
1160             label = self.wallet.labels.get(address,'')
1161             n = self.wallet.get_num_tx(address)
1162             item = QTreeWidgetItem( [ address, label, "%d"%n] )
1163             item.setFont(0, QFont(MONOSPACE_FONT))
1164             # 32 = label can be edited (bool)
1165             item.setData(0,32, True)
1166             # 33 = payto string
1167             item.setData(0,33, address)
1168             l.addTopLevelItem(item)
1169
1170         self.run_hook('update_contacts_tab', l)
1171         l.setCurrentItem(l.topLevelItem(0))
1172
1173
1174
1175     def create_console_tab(self):
1176         from qt_console import Console
1177         self.console = console = Console()
1178         self.console.history = self.config.get("console-history",[])
1179         self.console.history_index = len(self.console.history)
1180
1181         console.updateNamespace({'wallet' : self.wallet, 'interface' : self.wallet.interface, 'gui':self})
1182         console.updateNamespace({'util' : util, 'bitcoin':bitcoin})
1183
1184         c = commands.Commands(self.wallet, self.wallet.interface, lambda: self.console.set_json(True))
1185         methods = {}
1186         def mkfunc(f, method):
1187             return lambda *args: apply( f, (method, args, self.password_dialog ))
1188         for m in dir(c):
1189             if m[0]=='_' or m=='wallet' or m == 'interface': continue
1190             methods[m] = mkfunc(c._run, m)
1191             
1192         console.updateNamespace(methods)
1193         return console
1194
1195     def change_account(self,s):
1196         if s == _("All accounts"):
1197             self.current_account = None
1198         else:
1199             accounts = self.wallet.get_accounts()
1200             for k, v in accounts.items():
1201                 if v == s:
1202                     self.current_account = k
1203         self.update_history_tab()
1204         self.update_status()
1205         self.update_receive_tab()
1206
1207     def create_status_bar(self):
1208         self.status_text = ""
1209         sb = QStatusBar()
1210         sb.setFixedHeight(35)
1211         qtVersion = qVersion()
1212
1213         update_notification = UpdateLabel(self.config)
1214         if(update_notification.new_version):
1215             sb.addPermanentWidget(update_notification)
1216
1217         accounts = self.wallet.get_accounts()
1218         if len(accounts) > 1:
1219             from_combo = QComboBox()
1220             from_combo.addItems([_("All accounts")] + accounts.values())
1221             from_combo.setCurrentIndex(0)
1222             self.connect(from_combo,SIGNAL("activated(QString)"),self.change_account) 
1223             sb.addPermanentWidget(from_combo)
1224
1225         if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1226             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
1227         if self.wallet.seed:
1228             self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
1229             self.password_button = StatusBarButton( self.lock_icon, _("Password"), lambda: self.change_password_dialog(self.wallet, self) )
1230             sb.addPermanentWidget( self.password_button )
1231         sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
1232         if self.wallet.seed:
1233             sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), _("Seed"), self.show_seed_dialog ) )
1234         self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), _("Network"), lambda: self.network_dialog(self.wallet, self) ) 
1235         sb.addPermanentWidget( self.status_button )
1236
1237         self.run_hook('create_status_bar', (sb,))
1238
1239         self.setStatusBar(sb)
1240         
1241     def go_lite(self):
1242         import gui_lite
1243         self.config.set_key('gui', 'lite', True)
1244         self.hide()
1245         if self.lite:
1246             self.lite.mini.show()
1247         else:
1248             self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1249             self.lite.main(None)
1250
1251     def new_contact_dialog(self):
1252         text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1253         address = unicode(text)
1254         if ok:
1255             if is_valid(address):
1256                 self.wallet.addressbook.append(address)
1257                 self.wallet.save()
1258                 self.update_contacts_tab()
1259                 self.update_history_tab()
1260                 self.update_completions()
1261             else:
1262                 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1263
1264     def show_master_public_key(self):
1265         dialog = QDialog(self)
1266         dialog.setModal(1)
1267         dialog.setWindowTitle(_("Master Public Key"))
1268
1269         main_text = QTextEdit()
1270         main_text.setText(self.wallet.get_master_public_key())
1271         main_text.setReadOnly(True)
1272         main_text.setMaximumHeight(170)
1273         qrw = QRCodeWidget(self.wallet.get_master_public_key())
1274
1275         ok_button = QPushButton(_("OK"))
1276         ok_button.setDefault(True)
1277         ok_button.clicked.connect(dialog.accept)
1278
1279         main_layout = QGridLayout()
1280         main_layout.addWidget(QLabel(_('Your Master Public Key is:')), 0, 0, 1, 2)
1281
1282         main_layout.addWidget(main_text, 1, 0)
1283         main_layout.addWidget(qrw, 1, 1 )
1284
1285         vbox = QVBoxLayout()
1286         vbox.addLayout(main_layout)
1287         hbox = QHBoxLayout()
1288         hbox.addStretch(1)
1289         hbox.addWidget(ok_button)
1290         vbox.addLayout(hbox)
1291
1292         dialog.setLayout(vbox)
1293         dialog.exec_()
1294         
1295
1296     @protected
1297     def show_seed_dialog(self, password):
1298         if not self.wallet.seed:
1299             QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
1300             return
1301         try:
1302             seed = self.wallet.decode_seed(password)
1303         except:
1304             QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
1305             return
1306         self.show_seed(seed, self.wallet.imported_keys, self)
1307
1308
1309     @classmethod
1310     def show_seed(self, seed, imported_keys, parent=None):
1311         dialog = QDialog(parent)
1312         dialog.setModal(1)
1313         dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
1314
1315         brainwallet = ' '.join(mnemonic.mn_encode(seed))
1316
1317         label1 = QLabel(_("Your wallet generation seed is")+ ":")
1318
1319         seed_text = QTextEdit(brainwallet)
1320         seed_text.setReadOnly(True)
1321         seed_text.setMaximumHeight(130)
1322         
1323         msg2 =  _("Please write down or memorize these 12 words (order is important).") + " " \
1324               + _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
1325               + _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
1326               + "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
1327         if imported_keys:
1328             msg2 += "<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"
1329         label2 = QLabel(msg2)
1330         label2.setWordWrap(True)
1331
1332         logo = QLabel()
1333         logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1334         logo.setMaximumWidth(60)
1335
1336         qrw = QRCodeWidget(seed)
1337
1338         ok_button = QPushButton(_("OK"))
1339         ok_button.setDefault(True)
1340         ok_button.clicked.connect(dialog.accept)
1341
1342         grid = QGridLayout()
1343         #main_layout.addWidget(logo, 0, 0)
1344
1345         grid.addWidget(logo, 0, 0)
1346         grid.addWidget(label1, 0, 1)
1347
1348         grid.addWidget(seed_text, 1, 0, 1, 2)
1349
1350         grid.addWidget(qrw, 0, 2, 2, 1)
1351
1352         vbox = QVBoxLayout()
1353         vbox.addLayout(grid)
1354         vbox.addWidget(label2)
1355
1356         hbox = QHBoxLayout()
1357         hbox.addStretch(1)
1358         hbox.addWidget(ok_button)
1359         vbox.addLayout(hbox)
1360
1361         dialog.setLayout(vbox)
1362         dialog.exec_()
1363
1364     def show_qrcode(self, data, title = "QR code"):
1365         if not data: return
1366         d = QDialog(self)
1367         d.setModal(1)
1368         d.setWindowTitle(title)
1369         d.setMinimumSize(270, 300)
1370         vbox = QVBoxLayout()
1371         qrw = QRCodeWidget(data)
1372         vbox.addWidget(qrw, 1)
1373         vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1374         hbox = QHBoxLayout()
1375         hbox.addStretch(1)
1376
1377         def print_qr(self):
1378             filename = "qrcode.bmp"
1379             bmp.save_qrcode(qrw.qr, filename)
1380             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1381
1382         b = QPushButton(_("Save"))
1383         hbox.addWidget(b)
1384         b.clicked.connect(print_qr)
1385
1386         b = QPushButton(_("Close"))
1387         hbox.addWidget(b)
1388         b.clicked.connect(d.accept)
1389         b.setDefault(True)
1390
1391         vbox.addLayout(hbox)
1392         d.setLayout(vbox)
1393         d.exec_()
1394
1395
1396     def do_protect(self, func, args):
1397         if self.wallet.use_encryption:
1398             password = self.password_dialog()
1399             if not password:
1400                 return
1401         else:
1402             password = None
1403             
1404         if args != (False,):
1405             args = (self,) + args + (password,)
1406         else:
1407             args = (self,password)
1408         apply( func, args)
1409
1410
1411     @protected
1412     def show_private_key(self, address, password):
1413         if not address: return
1414         try:
1415             pk = self.wallet.get_private_key(address, password)
1416         except BaseException, e:
1417             self.show_message(str(e))
1418             return
1419         QMessageBox.information(self, _('Private key'), 'Address'+ ': ' + address + '\n\n' + _('Private key') + ': ' + pk, _('OK'))
1420
1421
1422     @protected
1423     def do_sign(self, address, message, signature, password):
1424         try:
1425             sig = self.wallet.sign_message(str(address.text()), str(message.toPlainText()), password)
1426             signature.setText(sig)
1427         except BaseException, e:
1428             self.show_message(str(e))
1429
1430     def sign_message(self, address):
1431         if not address: return
1432         d = QDialog(self)
1433         d.setModal(1)
1434         d.setWindowTitle(_('Sign Message'))
1435         d.setMinimumSize(410, 290)
1436
1437         tab_widget = QTabWidget()
1438         tab = QWidget()
1439         layout = QGridLayout(tab)
1440
1441         sign_address = QLineEdit()
1442
1443         sign_address.setText(address)
1444         layout.addWidget(QLabel(_('Address')), 1, 0)
1445         layout.addWidget(sign_address, 1, 1)
1446
1447         sign_message = QTextEdit()
1448         layout.addWidget(QLabel(_('Message')), 2, 0)
1449         layout.addWidget(sign_message, 2, 1)
1450         layout.setRowStretch(2,3)
1451
1452         sign_signature = QTextEdit()
1453         layout.addWidget(QLabel(_('Signature')), 3, 0)
1454         layout.addWidget(sign_signature, 3, 1)
1455         layout.setRowStretch(3,1)
1456
1457
1458         hbox = QHBoxLayout()
1459         b = QPushButton(_("Sign"))
1460         hbox.addWidget(b)
1461         b.clicked.connect(lambda: self.do_sign(sign_address, sign_message, sign_signature))
1462         b = QPushButton(_("Close"))
1463         b.clicked.connect(d.accept)
1464         hbox.addWidget(b)
1465         layout.addLayout(hbox, 4, 1)
1466         tab_widget.addTab(tab, _("Sign"))
1467
1468
1469         tab = QWidget()
1470         layout = QGridLayout(tab)
1471
1472         verify_address = QLineEdit()
1473         layout.addWidget(QLabel(_('Address')), 1, 0)
1474         layout.addWidget(verify_address, 1, 1)
1475
1476         verify_message = QTextEdit()
1477         layout.addWidget(QLabel(_('Message')), 2, 0)
1478         layout.addWidget(verify_message, 2, 1)
1479         layout.setRowStretch(2,3)
1480
1481         verify_signature = QTextEdit()
1482         layout.addWidget(QLabel(_('Signature')), 3, 0)
1483         layout.addWidget(verify_signature, 3, 1)
1484         layout.setRowStretch(3,1)
1485
1486         def do_verify():
1487             try:
1488                 self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
1489                 self.show_message(_("Signature verified"))
1490             except BaseException, e:
1491                 self.show_message(str(e))
1492                 return
1493
1494         hbox = QHBoxLayout()
1495         b = QPushButton(_("Verify"))
1496         b.clicked.connect(do_verify)
1497         hbox.addWidget(b)
1498         b = QPushButton(_("Close"))
1499         b.clicked.connect(d.accept)
1500         hbox.addWidget(b)
1501         layout.addLayout(hbox, 4, 1)
1502         tab_widget.addTab(tab, _("Verify"))
1503
1504         vbox = QVBoxLayout()
1505         vbox.addWidget(tab_widget)
1506         d.setLayout(vbox)
1507         d.exec_()
1508
1509         
1510
1511
1512     def question(self, msg):
1513         return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1514
1515     def show_message(self, msg):
1516         QMessageBox.information(self, _('Message'), msg, _('OK'))
1517
1518     def password_dialog(self ):
1519         d = QDialog(self)
1520         d.setModal(1)
1521
1522         pw = QLineEdit()
1523         pw.setEchoMode(2)
1524
1525         vbox = QVBoxLayout()
1526         msg = _('Please enter your password')
1527         vbox.addWidget(QLabel(msg))
1528
1529         grid = QGridLayout()
1530         grid.setSpacing(8)
1531         grid.addWidget(QLabel(_('Password')), 1, 0)
1532         grid.addWidget(pw, 1, 1)
1533         vbox.addLayout(grid)
1534
1535         vbox.addLayout(ok_cancel_buttons(d))
1536         d.setLayout(vbox)
1537
1538         self.run_hook('password_dialog', pw, grid, 1)
1539         if not d.exec_(): return
1540         return unicode(pw.text())
1541
1542
1543
1544
1545
1546     @staticmethod
1547     def change_password_dialog( wallet, parent=None ):
1548
1549         if not wallet.seed:
1550             QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1551             return
1552
1553         d = QDialog(parent)
1554         d.setModal(1)
1555
1556         pw = QLineEdit()
1557         pw.setEchoMode(2)
1558         new_pw = QLineEdit()
1559         new_pw.setEchoMode(2)
1560         conf_pw = QLineEdit()
1561         conf_pw.setEchoMode(2)
1562
1563         vbox = QVBoxLayout()
1564         if parent:
1565             msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1566                    +_('To disable wallet encryption, enter an empty new password.')) \
1567                    if wallet.use_encryption else _('Your wallet keys are not encrypted')
1568         else:
1569             msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1570                   +_("Leave these fields empty if you want to disable encryption.")
1571         vbox.addWidget(QLabel(msg))
1572
1573         grid = QGridLayout()
1574         grid.setSpacing(8)
1575
1576         if wallet.use_encryption:
1577             grid.addWidget(QLabel(_('Password')), 1, 0)
1578             grid.addWidget(pw, 1, 1)
1579
1580         grid.addWidget(QLabel(_('New Password')), 2, 0)
1581         grid.addWidget(new_pw, 2, 1)
1582
1583         grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1584         grid.addWidget(conf_pw, 3, 1)
1585         vbox.addLayout(grid)
1586
1587         vbox.addLayout(ok_cancel_buttons(d))
1588         d.setLayout(vbox) 
1589
1590         if not d.exec_(): return
1591
1592         password = unicode(pw.text()) if wallet.use_encryption else None
1593         new_password = unicode(new_pw.text())
1594         new_password2 = unicode(conf_pw.text())
1595
1596         try:
1597             seed = wallet.decode_seed(password)
1598         except:
1599             QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1600             return
1601
1602         if new_password != new_password2:
1603             QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1604             return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1605
1606         wallet.update_password(seed, password, new_password)
1607         if parent: 
1608             icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png")
1609             parent.password_button.setIcon( icon )
1610
1611
1612     @staticmethod
1613     def seed_dialog(wallet, parent=None):
1614         d = QDialog(parent)
1615         d.setModal(1)
1616
1617         vbox = QVBoxLayout()
1618         msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + '\n')
1619         vbox.addWidget(QLabel(msg))
1620
1621         grid = QGridLayout()
1622         grid.setSpacing(8)
1623
1624         seed_e = QLineEdit()
1625         grid.addWidget(QLabel(_('Seed or master public key')), 1, 0)
1626         grid.addWidget(seed_e, 1, 1)
1627         grid.addWidget(HelpButton(_("Your seed can be entered as a mnemonic (sequence of words), or as a hexadecimal string.")), 1, 3)
1628
1629         gap_e = QLineEdit()
1630         gap_e.setText("5")
1631         grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1632         grid.addWidget(gap_e, 2, 1)
1633         grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
1634         gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1635         vbox.addLayout(grid)
1636
1637         vbox.addLayout(ok_cancel_buttons(d))
1638         d.setLayout(vbox) 
1639
1640         if not d.exec_(): return
1641
1642         try:
1643             gap = int(unicode(gap_e.text()))
1644         except:
1645             QMessageBox.warning(None, _('Error'), 'error', 'OK')
1646             return
1647
1648         try:
1649             seed = str(seed_e.text())
1650             seed.decode('hex')
1651         except:
1652             print_error("Warning: Not hex, trying decode")
1653             try:
1654                 seed = mnemonic.mn_decode( seed.split(' ') )
1655             except:
1656                 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1657                 return
1658
1659         if not seed:
1660             QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
1661             return
1662
1663         return seed, gap
1664
1665     def generate_transaction_information_widget(self, tx):
1666         tabs = QTabWidget(self)
1667
1668         tab1 = QWidget()
1669         grid_ui = QGridLayout(tab1)
1670         grid_ui.setColumnStretch(0,1)
1671         tabs.addTab(tab1, _('Outputs') )
1672
1673         tree_widget = MyTreeWidget(self)
1674         tree_widget.setColumnCount(2)
1675         tree_widget.setHeaderLabels( [_('Address'), _('Amount')] )
1676         tree_widget.setColumnWidth(0, 300)
1677         tree_widget.setColumnWidth(1, 50)
1678
1679         for address, value in tx.outputs:
1680             item = QTreeWidgetItem( [address, "%s" % ( format_satoshis(value))] )
1681             tree_widget.addTopLevelItem(item)
1682
1683         tree_widget.setMaximumHeight(100)
1684
1685         grid_ui.addWidget(tree_widget)
1686
1687         tab2 = QWidget()
1688         grid_ui = QGridLayout(tab2)
1689         grid_ui.setColumnStretch(0,1)
1690         tabs.addTab(tab2, _('Inputs') )
1691         
1692         tree_widget = MyTreeWidget(self)
1693         tree_widget.setColumnCount(2)
1694         tree_widget.setHeaderLabels( [ _('Address'), _('Previous output')] )
1695
1696         for input_line in tx.inputs:
1697             item = QTreeWidgetItem( [ str(input_line["address"]), str(input_line["prevout_hash"])] )
1698             tree_widget.addTopLevelItem(item)
1699
1700         tree_widget.setMaximumHeight(100)
1701
1702         grid_ui.addWidget(tree_widget)
1703         return tabs
1704
1705
1706     def tx_dict_from_text(self, txt):
1707         try:
1708             tx_dict = json.loads(str(txt))
1709             assert "hex" in tx_dict.keys()
1710             assert "complete" in tx_dict.keys()
1711             if not tx_dict["complete"]:
1712                 assert "input_info" in tx_dict.keys()
1713         except:
1714             QMessageBox.critical(None, "Unable to parse transaction", _("Electrum was unable to parse your transaction"))
1715             return None
1716         return tx_dict
1717
1718
1719     def read_tx_from_file(self):
1720         fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
1721         if not fileName:
1722             return
1723         try:
1724             with open(fileName, "r") as f:
1725                 file_content = f.read()
1726         except (ValueError, IOError, os.error), reason:
1727             QMessageBox.critical(None,"Unable to read file or no transaction found", _("Electrum was unable to open your transaction file") + "\n" + str(reason))
1728
1729         return self.tx_dict_from_text(file_content)
1730
1731
1732     @protected
1733     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
1734         try:
1735             self.wallet.signrawtransaction(tx, input_info, [], password)
1736             
1737             fileName = self.getSaveFileName(_("Select where to save your signed transaction"), 'signed_%s.txn' % (tx.hash()[0:8]), "*.txn")
1738             if fileName:
1739                 with open(fileName, "w+") as f:
1740                     f.write(json.dumps(tx.as_dict(),indent=4) + '\n')
1741                 self.show_message(_("Transaction saved successfully"))
1742                 if dialog:
1743                     dialog.done(0)
1744         except BaseException, e:
1745             self.show_message(str(e))
1746     
1747
1748     def send_raw_transaction(self, raw_tx, dialog = ""):
1749         result, result_message = self.wallet.sendtx( raw_tx )
1750         if result:
1751             self.show_message("Transaction successfully sent: %s" % (result_message))
1752             if dialog:
1753                 dialog.done(0)
1754         else:
1755             self.show_message("There was a problem sending your transaction:\n %s" % (result_message))
1756
1757     def do_process_from_text(self):
1758         text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction"))
1759         if not text:
1760             return
1761         tx_dict = self.tx_dict_from_text(text)
1762         if tx_dict:
1763             self.create_process_transaction_window(tx_dict)
1764
1765     def do_process_from_file(self):
1766         tx_dict = self.read_tx_from_file()
1767         if tx_dict: 
1768             self.create_process_transaction_window(tx_dict)
1769
1770     def create_process_transaction_window(self, tx_dict):
1771         tx = Transaction(tx_dict["hex"])
1772             
1773         dialog = QDialog(self)
1774         dialog.setMinimumWidth(500)
1775         dialog.setWindowTitle(_('Process raw transaction'))
1776         dialog.setModal(1)
1777
1778         l = QGridLayout()
1779         dialog.setLayout(l)
1780
1781         l.addWidget(QLabel(_("Transaction status:")), 3,0)
1782         l.addWidget(QLabel(_("Actions")), 4,0)
1783
1784         if tx_dict["complete"] == False:
1785             l.addWidget(QLabel(_("Unsigned")), 3,1)
1786             if self.wallet.seed :
1787                 b = QPushButton("Sign transaction")
1788                 input_info = json.loads(tx_dict["input_info"])
1789                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
1790                 l.addWidget(b, 4, 1)
1791             else:
1792                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
1793         else:
1794             l.addWidget(QLabel(_("Signed")), 3,1)
1795             b = QPushButton("Broadcast transaction")
1796             b.clicked.connect(lambda: self.send_raw_transaction(tx, dialog))
1797             l.addWidget(b,4,1)
1798
1799         l.addWidget( self.generate_transaction_information_widget(tx), 0,0,2,3)
1800         cancelButton = QPushButton(_("Cancel"))
1801         cancelButton.clicked.connect(lambda: dialog.done(0))
1802         l.addWidget(cancelButton, 4,2)
1803
1804         dialog.exec_()
1805
1806
1807     @protected
1808     def do_export_privkeys(self, password):
1809         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.")))
1810
1811         try:
1812             select_export = _('Select file to export your private keys to')
1813             fileName = self.getSaveFileName(select_export, 'electrum-private-keys.csv', "*.csv")
1814             if fileName:
1815                 with open(fileName, "w+") as csvfile:
1816                     transaction = csv.writer(csvfile)
1817                     transaction.writerow(["address", "private_key"])
1818
1819                     
1820                     for addr, pk in self.wallet.get_private_keys(self.wallet.addresses(True), password).items():
1821                         transaction.writerow(["%34s"%addr,pk])
1822
1823                     self.show_message(_("Private keys exported."))
1824
1825         except (IOError, os.error), reason:
1826             export_error_label = _("Electrum was unable to produce a private key-export.")
1827             QMessageBox.critical(None,"Unable to create csv", export_error_label + "\n" + str(reason))
1828
1829         except BaseException, e:
1830           self.show_message(str(e))
1831           return
1832
1833
1834     def do_import_labels(self):
1835         labelsFile = self.getOpenFileName(_("Open labels file"), "*.dat")
1836         if not labelsFile: return
1837         try:
1838             f = open(labelsFile, 'r')
1839             data = f.read()
1840             f.close()
1841             for key, value in json.loads(data).items():
1842                 self.wallet.labels[key] = value
1843             self.wallet.save()
1844             QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
1845         except (IOError, os.error), reason:
1846             QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
1847             
1848
1849     def do_export_labels(self):
1850         labels = self.wallet.labels
1851         try:
1852             fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.dat', "*.dat")
1853             if fileName:
1854                 with open(fileName, 'w+') as f:
1855                     json.dump(labels, f)
1856                 QMessageBox.information(None, "Labels exported", _("Your labels where exported to")+" '%s'" % str(fileName))
1857         except (IOError, os.error), reason:
1858             QMessageBox.critical(None, "Unable to export labels", _("Electrum was unable to export your labels.")+"\n" + str(reason))
1859
1860
1861     def do_export_history(self):
1862         from gui_lite import csv_transaction
1863         csv_transaction(self.wallet)
1864
1865
1866     @protected
1867     def do_import_privkey(self, password):
1868         if not self.wallet.imported_keys:
1869             r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
1870                                          + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
1871                                          + _('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>' \
1872                                          + _('Are you sure you understand what you are doing?'), 3, 4)
1873             if r == 4: return
1874
1875         text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
1876         if not text: return
1877
1878         text = str(text).split()
1879         badkeys = []
1880         addrlist = []
1881         for key in text:
1882             try:
1883                 addr = self.wallet.import_key(key, password)
1884             except BaseException as e:
1885                 badkeys.append(key)
1886                 continue
1887             if not addr: 
1888                 badkeys.append(key)
1889             else:
1890                 addrlist.append(addr)
1891         if addrlist:
1892             QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
1893         if badkeys:
1894             QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
1895         self.update_receive_tab()
1896         self.update_history_tab()
1897
1898
1899     def settings_dialog(self):
1900         d = QDialog(self)
1901         d.setWindowTitle(_('Electrum Settings'))
1902         d.setModal(1)
1903         vbox = QVBoxLayout()
1904
1905         tabs = QTabWidget(self)
1906         self.settings_tab = tabs
1907         vbox.addWidget(tabs)
1908
1909         tab1 = QWidget()
1910         grid_ui = QGridLayout(tab1)
1911         grid_ui.setColumnStretch(0,1)
1912         tabs.addTab(tab1, _('Display') )
1913
1914         nz_label = QLabel(_('Display zeros'))
1915         grid_ui.addWidget(nz_label, 0, 0)
1916         nz_e = QLineEdit()
1917         nz_e.setText("%d"% self.wallet.num_zeros)
1918         grid_ui.addWidget(nz_e, 0, 1)
1919         msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1920         grid_ui.addWidget(HelpButton(msg), 0, 2)
1921         nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1922         if not self.config.is_modifiable('num_zeros'):
1923             for w in [nz_e, nz_label]: w.setEnabled(False)
1924         
1925         lang_label=QLabel(_('Language') + ':')
1926         grid_ui.addWidget(lang_label, 1, 0)
1927         lang_combo = QComboBox()
1928         from i18n import languages
1929         lang_combo.addItems(languages.values())
1930         try:
1931             index = languages.keys().index(self.config.get("language",''))
1932         except:
1933             index = 0
1934         lang_combo.setCurrentIndex(index)
1935         grid_ui.addWidget(lang_combo, 1, 1)
1936         grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart).')+' '), 1, 2)
1937         if not self.config.is_modifiable('language'):
1938             for w in [lang_combo, lang_label]: w.setEnabled(False)
1939
1940         currencies = self.exchanger.get_currencies()
1941         currencies.insert(0, "None")
1942
1943         cur_label=QLabel(_('Currency') + ':')
1944         grid_ui.addWidget(cur_label , 2, 0)
1945         cur_combo = QComboBox()
1946         cur_combo.addItems(currencies)
1947         try:
1948             index = currencies.index(self.config.get('currency', "None"))
1949         except:
1950             index = 0
1951         cur_combo.setCurrentIndex(index)
1952         grid_ui.addWidget(cur_combo, 2, 1)
1953         grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes.')+' '), 2, 2)
1954         
1955         expert_cb = QCheckBox(_('Expert mode'))
1956         expert_cb.setChecked(self.expert_mode)
1957         grid_ui.addWidget(expert_cb, 3, 0)
1958         hh =  _('In expert mode, your client will:') + '\n'  \
1959             + _(' - Show change addresses in the Receive tab') + '\n'  \
1960             + _(' - Display the balance of each address') + '\n'  \
1961             + _(' - Add freeze/prioritize actions to addresses.') 
1962         grid_ui.addWidget(HelpButton(hh), 3, 2)
1963         grid_ui.setRowStretch(4,1)
1964
1965         # wallet tab
1966         tab2 = QWidget()
1967         grid_wallet = QGridLayout(tab2)
1968         grid_wallet.setColumnStretch(0,1)
1969         tabs.addTab(tab2, _('Wallet') )
1970         
1971         fee_label = QLabel(_('Transaction fee'))
1972         grid_wallet.addWidget(fee_label, 0, 0)
1973         fee_e = QLineEdit()
1974         fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1975         grid_wallet.addWidget(fee_e, 0, 2)
1976         msg = _('Fee per kilobyte of transaction.') + ' ' \
1977             + _('Recommended value') + ': 0.0002'
1978         grid_wallet.addWidget(HelpButton(msg), 0, 3)
1979         fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1980         if not self.config.is_modifiable('fee_per_kb'):
1981             for w in [fee_e, fee_label]: w.setEnabled(False)
1982
1983         usechange_label = QLabel(_('Use change addresses'))
1984         grid_wallet.addWidget(usechange_label, 1, 0)
1985         usechange_combo = QComboBox()
1986         usechange_combo.addItems([_('Yes'), _('No')])
1987         usechange_combo.setCurrentIndex(not self.wallet.use_change)
1988         grid_wallet.addWidget(usechange_combo, 1, 2)
1989         grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions.')+' '), 1, 3)
1990         if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1991
1992         gap_label = QLabel(_('Gap limit'))
1993         grid_wallet.addWidget(gap_label, 2, 0)
1994         gap_e = QLineEdit()
1995         gap_e.setText("%d"% self.wallet.gap_limit)
1996         grid_wallet.addWidget(gap_e, 2, 2)
1997         msg =  _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1998               + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1999               + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
2000               + _('Given the current status of your address sequence, the minimum gap limit you can use is:')+' ' + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
2001               + _('Warning') + ': ' \
2002               + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
2003               + _('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' 
2004         grid_wallet.addWidget(HelpButton(msg), 2, 3)
2005         gap_e.textChanged.connect(lambda: numbify(nz_e,True))
2006         if not self.config.is_modifiable('gap_limit'):
2007             for w in [gap_e, gap_label]: w.setEnabled(False)
2008
2009         grid_wallet.setRowStretch(3,1)
2010
2011
2012         # import/export tab
2013         tab3 = QWidget()
2014         grid_io = QGridLayout(tab3)
2015         grid_io.setColumnStretch(0,1)
2016         tabs.addTab(tab3, _('Import/Export') )
2017         
2018         grid_io.addWidget(QLabel(_('Labels')), 1, 0)
2019         grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
2020         grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
2021         grid_io.addWidget(HelpButton(_('Export your labels as json')), 1, 3)
2022
2023         grid_io.addWidget(QLabel(_('History')), 2, 0)
2024         grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
2025         grid_io.addWidget(HelpButton(_('Export your transaction history as csv')), 2, 3)
2026
2027         grid_io.addWidget(QLabel(_('Private keys')), 3, 0)
2028
2029         grid_io.addWidget(EnterButton(_("Export"), self.do_export_privkeys), 3, 1)
2030         grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
2031         grid_io.addWidget(HelpButton(_('Import private key')), 3, 3)
2032
2033         grid_io.addWidget(QLabel(_('Master Public Key')), 4, 0)
2034         grid_io.addWidget(EnterButton(_("Show"), self.show_master_public_key), 4, 1)
2035         grid_io.addWidget(HelpButton(_('Your Master Public Key can be used to create receiving addresses, but not to sign transactions.') + ' ' \
2036                               + _('If you give it to someone, they will be able to see your transactions, but not to spend your money.') + ' ' \
2037                               + _('If you restore your wallet from it, a watching-only (deseeded) wallet will be created.')), 4, 3)
2038
2039
2040         grid_io.addWidget(QLabel(_("Load transaction")), 5, 0)
2041         grid_io.addWidget(EnterButton(_("From file"), self.do_process_from_file), 5, 1)
2042         grid_io.addWidget(EnterButton(_("From text"), self.do_process_from_text), 5, 2)
2043         grid_io.addWidget(HelpButton(_("This will give you the option to sign or broadcast a transaction based on it's status.")), 5, 3)
2044
2045         grid_io.setRowStretch(5,1)
2046
2047
2048         # plugins
2049         if self.plugins:
2050             tab5 = QScrollArea()
2051             grid_plugins = QGridLayout(tab5)
2052             grid_plugins.setColumnStretch(0,1)
2053             tabs.addTab(tab5, _('Plugins') )
2054             def mk_toggle(cb, p):
2055                 return lambda: cb.setChecked(p.toggle())
2056             for i, p in enumerate(self.plugins):
2057                 try:
2058                     name, description = p.get_info()
2059                     cb = QCheckBox(name)
2060                     cb.setDisabled(not p.is_available())
2061                     cb.setChecked(p.is_enabled())
2062                     cb.clicked.connect(mk_toggle(cb,p))
2063                     grid_plugins.addWidget(cb, i, 0)
2064                     if p.requires_settings():
2065                         grid_plugins.addWidget(EnterButton(_('Settings'), p.settings_dialog), i, 1)
2066                     grid_plugins.addWidget(HelpButton(description), i, 2)
2067                 except:
2068                     print_msg("Error: cannot display plugin", p)
2069                     traceback.print_exc(file=sys.stdout)
2070             grid_plugins.setRowStretch(i+1,1)
2071
2072         self.run_hook('create_settings_tab', tabs)
2073
2074         vbox.addLayout(ok_cancel_buttons(d))
2075         d.setLayout(vbox) 
2076
2077         # run the dialog
2078         if not d.exec_(): return
2079
2080         fee = unicode(fee_e.text())
2081         try:
2082             fee = int( 100000000 * Decimal(fee) )
2083         except:
2084             QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
2085             return
2086
2087         if self.wallet.fee != fee:
2088             self.wallet.fee = fee
2089             self.wallet.save()
2090         
2091         nz = unicode(nz_e.text())
2092         try:
2093             nz = int( nz )
2094             if nz>8: nz=8
2095         except:
2096             QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
2097             return
2098
2099         if self.wallet.num_zeros != nz:
2100             self.wallet.num_zeros = nz
2101             self.config.set_key('num_zeros', nz, True)
2102             self.update_history_tab()
2103             self.update_receive_tab()
2104
2105         usechange_result = usechange_combo.currentIndex() == 0
2106         if self.wallet.use_change != usechange_result:
2107             self.wallet.use_change = usechange_result
2108             self.config.set_key('use_change', self.wallet.use_change, True)
2109         
2110         try:
2111             n = int(gap_e.text())
2112         except:
2113             QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2114             return
2115
2116         if self.wallet.gap_limit != n:
2117             r = self.wallet.change_gap_limit(n)
2118             if r:
2119                 self.update_receive_tab()
2120                 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
2121             else:
2122                 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
2123
2124         need_restart = False
2125
2126         lang_request = languages.keys()[lang_combo.currentIndex()]
2127         if lang_request != self.config.get('language'):
2128             self.config.set_key("language", lang_request, True)
2129             need_restart = True
2130             
2131         cur_request = str(currencies[cur_combo.currentIndex()])
2132         if cur_request != self.config.get('currency', "None"):
2133             self.config.set_key('currency', cur_request, True)
2134             self.update_wallet()
2135
2136         self.run_hook('close_settings_dialog')
2137
2138         if need_restart:
2139             QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
2140
2141         self.receive_tab_set_mode(expert_cb.isChecked())
2142
2143
2144     @staticmethod 
2145     def network_dialog(wallet, parent=None):
2146         interface = wallet.interface
2147         if parent:
2148             if interface.is_connected:
2149                 status = _("Connected to")+" %s\n%d "%(interface.host, wallet.verifier.height)+_("blocks")
2150             else:
2151                 status = _("Not connected")
2152             server = interface.server
2153         else:
2154             import random
2155             status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
2156             server = interface.server
2157
2158         plist, servers_list = interface.get_servers_list()
2159
2160         d = QDialog(parent)
2161         d.setModal(1)
2162         d.setWindowTitle(_('Server'))
2163         d.setMinimumSize(375, 20)
2164
2165         vbox = QVBoxLayout()
2166         vbox.setSpacing(30)
2167
2168         hbox = QHBoxLayout()
2169         l = QLabel()
2170         l.setPixmap(QPixmap(":icons/network.png"))
2171         hbox.addStretch(10)
2172         hbox.addWidget(l)
2173         hbox.addWidget(QLabel(status))
2174         hbox.addStretch(50)
2175         vbox.addLayout(hbox)
2176
2177
2178         # grid layout
2179         grid = QGridLayout()
2180         grid.setSpacing(8)
2181         vbox.addLayout(grid)
2182
2183         # server
2184         server_protocol = QComboBox()
2185         server_host = QLineEdit()
2186         server_host.setFixedWidth(200)
2187         server_port = QLineEdit()
2188         server_port.setFixedWidth(60)
2189
2190         protocol_names = ['TCP', 'HTTP', 'SSL', 'HTTPS']
2191         protocol_letters = 'thsg'
2192         server_protocol.addItems(protocol_names)
2193
2194         grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
2195         grid.addWidget(server_protocol, 0, 1)
2196         grid.addWidget(server_host, 0, 2)
2197         grid.addWidget(server_port, 0, 3)
2198
2199         def change_protocol(p):
2200             protocol = protocol_letters[p]
2201             host = unicode(server_host.text())
2202             pp = plist.get(host,DEFAULT_PORTS)
2203             if protocol not in pp.keys():
2204                 protocol = pp.keys()[0]
2205             port = pp[protocol]
2206             server_host.setText( host )
2207             server_port.setText( port )
2208
2209         server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
2210         
2211         label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
2212         servers_list_widget = QTreeWidget(parent)
2213         servers_list_widget.setHeaderLabels( [ label, _('Pruning') ] )
2214         servers_list_widget.setMaximumHeight(150)
2215         servers_list_widget.setColumnWidth(0, 240)
2216         for _host in servers_list.keys():
2217             pruning_level = servers_list[_host].get('pruning')
2218             servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, pruning_level ] ))
2219         servers_list_widget.setColumnHidden(1, not parent.expert_mode if parent else True)
2220
2221         def change_server(host, protocol=None):
2222             pp = plist.get(host,DEFAULT_PORTS)
2223             if protocol:
2224                 port = pp.get(protocol)
2225                 if not port: protocol = None
2226                     
2227             if not protocol:
2228                 if 's' in pp.keys():
2229                     protocol = 's'
2230                     port = pp.get(protocol)
2231                 else:
2232                     protocol = pp.keys()[0]
2233                     port = pp.get(protocol)
2234             
2235             server_host.setText( host )
2236             server_port.setText( port )
2237             server_protocol.setCurrentIndex(protocol_letters.index(protocol))
2238
2239             if not plist: return
2240             for p in protocol_letters:
2241                 i = protocol_letters.index(p)
2242                 j = server_protocol.model().index(i,0)
2243                 if p not in pp.keys() and interface.is_connected:
2244                     server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
2245                 else:
2246                     server_protocol.model().setData(j, QtCore.QVariant(33), QtCore.Qt.UserRole-1)
2247
2248
2249         if server:
2250             host, port, protocol = server.split(':')
2251             change_server(host,protocol)
2252
2253         servers_list_widget.connect(servers_list_widget, SIGNAL('currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)'), 
2254                                     lambda x,y: change_server(unicode(x.text(0))))
2255         grid.addWidget(servers_list_widget, 1, 1, 1, 3)
2256
2257         if not wallet.config.is_modifiable('server'):
2258             for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
2259
2260         # auto cycle
2261         autocycle_cb = QCheckBox(_('Try random servers if disconnected'))
2262         autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
2263         grid.addWidget(autocycle_cb, 3, 1, 3, 2)
2264         if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
2265
2266         # proxy setting
2267         proxy_mode = QComboBox()
2268         proxy_host = QLineEdit()
2269         proxy_host.setFixedWidth(200)
2270         proxy_port = QLineEdit()
2271         proxy_port.setFixedWidth(60)
2272         proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
2273
2274         def check_for_disable(index = False):
2275             if proxy_mode.currentText() != 'NONE':
2276                 proxy_host.setEnabled(True)
2277                 proxy_port.setEnabled(True)
2278             else:
2279                 proxy_host.setEnabled(False)
2280                 proxy_port.setEnabled(False)
2281
2282         check_for_disable()
2283         proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
2284
2285         if not wallet.config.is_modifiable('proxy'):
2286             for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
2287
2288         proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
2289         proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
2290         proxy_host.setText(proxy_config.get("host"))
2291         proxy_port.setText(proxy_config.get("port"))
2292
2293         grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
2294         grid.addWidget(proxy_mode, 2, 1)
2295         grid.addWidget(proxy_host, 2, 2)
2296         grid.addWidget(proxy_port, 2, 3)
2297
2298         # buttons
2299         vbox.addLayout(ok_cancel_buttons(d))
2300         d.setLayout(vbox) 
2301
2302         if not d.exec_(): return
2303
2304         server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
2305         if proxy_mode.currentText() != 'NONE':
2306             proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
2307         else:
2308             proxy = None
2309
2310         wallet.config.set_key("proxy", proxy, True)
2311         wallet.config.set_key("server", server, True)
2312         interface.set_server(server, proxy)
2313         wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
2314         return True
2315
2316     def closeEvent(self, event):
2317         g = self.geometry()
2318         self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
2319         self.save_column_widths()
2320         self.config.set_key("column_widths", self.column_widths, True)
2321         self.config.set_key("console-history",self.console.history[-50:])
2322         event.accept()
2323
2324
2325 class ElectrumGui:
2326
2327     def __init__(self, wallet, config, app=None):
2328         self.wallet = wallet
2329         self.config = config
2330         if app is None:
2331             self.app = QApplication(sys.argv)
2332
2333
2334     def restore_or_create(self):
2335         msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2336         r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2337         if r==2: return None
2338         return 'restore' if r==1 else 'create'
2339
2340     def seed_dialog(self):
2341         return ElectrumWindow.seed_dialog( self.wallet )
2342
2343     def network_dialog(self):
2344         return ElectrumWindow.network_dialog( self.wallet, parent=None )
2345         
2346
2347     def show_seed(self):
2348         ElectrumWindow.show_seed(self.wallet.seed, self.wallet.imported_keys)
2349
2350
2351     def password_dialog(self):
2352         if self.wallet.seed:
2353             ElectrumWindow.change_password_dialog(self.wallet)
2354
2355
2356     def restore_wallet(self):
2357         wallet = self.wallet
2358         # wait until we are connected, because the user might have selected another server
2359         if not wallet.interface.is_connected:
2360             waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
2361             waiting_dialog(waiting)
2362
2363         waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
2364             %(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
2365
2366         wallet.set_up_to_date(False)
2367         wallet.interface.poke('synchronizer')
2368         waiting_dialog(waiting)
2369         if wallet.is_found():
2370             print_error( "Recovery successful" )
2371         else:
2372             QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2373
2374         return True
2375
2376     def main(self,url):
2377         s = Timer()
2378         s.start()
2379         w = ElectrumWindow(self.wallet, self.config)
2380         if url: w.set_url(url)
2381         w.app = self.app
2382         w.connect_slots(s)
2383         w.update_wallet()
2384         w.show()
2385
2386         self.app.exec_()
2387
2388