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