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