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