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