3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
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.
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.
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/>.
19 import sys, time, datetime, re
21 from util import print_error
22 import os.path, json, util
27 sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
32 import PyQt4.QtGui as QtGui
33 from interface import DEFAULT_SERVERS
38 sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'")
40 from wallet import format_satoshis
41 import bmp, mnemonic, pyqrnative, qrscanner
44 from decimal import Decimal
48 if platform.system() == 'Windows':
49 MONOSPACE_FONT = 'Lucida Console'
50 elif platform.system() == 'Darwin':
51 MONOSPACE_FONT = 'Monaco'
53 MONOSPACE_FONT = 'monospace'
55 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
57 def numbify(entry, is_int = False):
58 text = unicode(entry.text()).strip()
59 pos = entry.cursorPosition()
61 if not is_int: chars +='.'
62 s = ''.join([i for i in text if i in chars])
67 s = s[:p] + '.' + s[p:p+8]
69 amount = int( Decimal(s) * 100000000 )
78 entry.setCursorPosition(pos)
82 class Timer(QtCore.QThread):
85 self.emit(QtCore.SIGNAL('timersignal'))
88 class HelpButton(QPushButton):
89 def __init__(self, text):
90 QPushButton.__init__(self, '?')
91 self.setFocusPolicy(Qt.NoFocus)
92 self.setFixedWidth(20)
93 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
96 class EnterButton(QPushButton):
97 def __init__(self, text, func):
98 QPushButton.__init__(self, text)
100 self.clicked.connect(func)
102 def keyPressEvent(self, e):
103 if e.key() == QtCore.Qt.Key_Return:
106 class MyTreeWidget(QTreeWidget):
107 def __init__(self, parent):
108 QTreeWidget.__init__(self, parent)
111 for i in range(0,self.viewport().height()/5):
112 if self.itemAt(QPoint(0,i*5)) == item:
116 for j in range(0,30):
117 if self.itemAt(QPoint(0,i*5 + j)) != item:
119 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
121 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
126 class StatusBarButton(QPushButton):
127 def __init__(self, icon, tooltip, func):
128 QPushButton.__init__(self, icon, '')
129 self.setToolTip(tooltip)
131 self.setMaximumWidth(25)
132 self.clicked.connect(func)
135 def keyPressEvent(self, e):
136 if e.key() == QtCore.Qt.Key_Return:
140 class QRCodeWidget(QWidget):
142 def __init__(self, data = None):
143 QWidget.__init__(self)
144 self.setMinimumSize(210, 210)
151 def set_addr(self, addr):
152 if self.addr != addr:
158 if self.addr and not self.qr:
159 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
160 self.qr.addData(self.addr)
164 def paintEvent(self, e):
169 black = QColor(0, 0, 0, 255)
170 white = QColor(255, 255, 255, 255)
173 qp = QtGui.QPainter()
177 qp.drawRect(0, 0, 198, 198)
181 k = self.qr.getModuleCount()
182 qp = QtGui.QPainter()
185 boxsize = min(r.width(), r.height())*0.8/k
187 left = (r.width() - size)/2
188 top = (r.height() - size)/2
192 if self.qr.isDark(r, c):
198 qp.drawRect(left+c*boxsize, top+r*boxsize, boxsize, boxsize)
203 class QR_Window(QWidget):
206 QWidget.__init__(self)
207 self.setWindowTitle('Electrum - Invoice')
208 self.setMinimumSize(800, 250)
212 self.setFocusPolicy(QtCore.Qt.NoFocus)
214 main_box = QHBoxLayout()
216 self.qrw = QRCodeWidget()
217 main_box.addWidget(self.qrw, 1)
220 main_box.addLayout(vbox)
222 self.address_label = QLabel("")
223 self.address_label.setFont(QFont(MONOSPACE_FONT))
224 vbox.addWidget(self.address_label)
226 self.label_label = QLabel("")
227 vbox.addWidget(self.label_label)
229 self.amount_label = QLabel("")
230 vbox.addWidget(self.amount_label)
233 self.setLayout(main_box)
236 def set_content(self, addr, label, amount):
238 address_text = "<span style='font-size: 18pt'>%s</span>" % addr if addr else ""
239 self.address_label.setText(address_text)
242 amount_text = "<span style='font-size: 21pt'>%s</span> <span style='font-size: 16pt'>BTC</span> " % format_satoshis(amount) if amount else ""
243 self.amount_label.setText(amount_text)
246 label_text = "<span style='font-size: 21pt'>%s</span>" % label if label else ""
247 self.label_label.setText(label_text)
249 msg = 'bitcoin:'+self.address
250 if self.amount is not None:
251 msg += '?amount=%s'%(str( Decimal(self.amount) /100000000))
252 if self.label is not None:
253 msg += '&label=%s'%(self.label)
254 elif self.label is not None:
255 msg += '?label=%s'%(self.label)
257 self.qrw.set_addr( msg )
262 def waiting_dialog(f):
268 w.setWindowTitle('Electrum')
278 w.connect(s, QtCore.SIGNAL('timersignal'), ff)
283 def ok_cancel_buttons(dialog):
286 b = QPushButton("OK")
288 b.clicked.connect(dialog.accept)
289 b = QPushButton("Cancel")
291 b.clicked.connect(dialog.reject)
295 class ElectrumWindow(QMainWindow):
297 def __init__(self, wallet, config):
298 QMainWindow.__init__(self)
302 self.wallet.interface.register_callback('updated', self.update_callback)
303 self.wallet.interface.register_callback('connected', self.update_callback)
304 self.wallet.interface.register_callback('disconnected', self.update_callback)
305 self.wallet.interface.register_callback('disconnecting', self.update_callback)
307 self.receive_tab_mode = config.get('qt_receive_tab_mode', 0)
308 self.merchant_name = config.get('merchant_name', 'Invoice')
310 self.qr_window = None
311 self.funds_error = False
312 self.completions = QStringListModel()
314 self.tabs = tabs = QTabWidget(self)
315 tabs.addTab(self.create_history_tab(), _('History') )
316 tabs.addTab(self.create_send_tab(), _('Send') )
317 tabs.addTab(self.create_receive_tab(), _('Receive') )
318 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
319 tabs.addTab(self.create_wall_tab(), _('Wall') )
320 tabs.setMinimumSize(600, 400)
321 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
322 self.setCentralWidget(tabs)
323 self.create_status_bar()
324 self.toggle_QR_window(self.receive_tab_mode == 2)
326 g = self.config.get("winpos-qt",[100, 100, 840, 400])
327 self.setGeometry(g[0], g[1], g[2], g[3])
328 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
329 if not self.wallet.seed: title += ' [seedless]'
330 self.setWindowTitle( title )
332 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
333 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
334 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
335 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
337 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
338 #self.connect(self, SIGNAL('editamount'), self.edit_amount)
339 self.history_list.setFocus(True)
341 self.exchanger = exchange_rate.Exchanger(self)
342 self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
344 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
345 if platform.system() == 'Windows':
346 n = 3 if self.wallet.seed else 2
347 tabs.setCurrentIndex (n)
348 tabs.setCurrentIndex (0)
351 QMainWindow.close(self)
353 self.qr_window.close()
354 self.qr_window = None
356 def connect_slots(self, sender):
357 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
358 self.previous_payto_e=''
360 def timer_actions(self):
362 self.qr_window.qrw.update_qr()
364 if self.payto_e.hasFocus():
366 r = unicode( self.payto_e.text() )
367 if r != self.previous_payto_e:
368 self.previous_payto_e = r
370 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
372 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
376 s = r + ' <' + to_address + '>'
377 self.payto_e.setText(s)
380 def update_callback(self):
381 self.emit(QtCore.SIGNAL('updatesignal'))
383 def update_wallet(self):
384 if self.wallet.interface and self.wallet.interface.is_connected:
385 if not self.wallet.up_to_date:
386 text = _( "Synchronizing..." )
387 icon = QIcon(":icons/status_waiting.png")
389 c, u = self.wallet.get_balance()
390 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
391 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
392 text += self.create_quote_text(Decimal(c+u)/100000000)
393 icon = QIcon(":icons/status_connected.png")
395 text = _( "Not connected" )
396 icon = QIcon(":icons/status_disconnected.png")
398 self.status_text = text
399 self.statusBar().showMessage(text)
400 self.status_button.setIcon( icon )
402 if self.wallet.up_to_date or not self.wallet.interface.is_connected:
403 self.textbox.setText( self.wallet.banner )
404 self.update_history_tab()
405 self.update_receive_tab()
406 self.update_contacts_tab()
407 self.update_completions()
409 def create_quote_text(self, btc_balance):
410 quote_currency = self.config.get("currency", "None")
411 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
412 if quote_balance is None:
415 quote_text = " (%.2f %s)" % (quote_balance, quote_currency)
418 def create_history_tab(self):
419 self.history_list = l = MyTreeWidget(self)
421 l.setColumnWidth(0, 40)
422 l.setColumnWidth(1, 140)
423 l.setColumnWidth(2, 350)
424 l.setColumnWidth(3, 140)
425 l.setColumnWidth(4, 140)
426 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
427 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
428 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
430 l.setContextMenuPolicy(Qt.CustomContextMenu)
431 l.customContextMenuRequested.connect(self.create_history_menu)
435 def create_history_menu(self, position):
436 self.history_list.selectedIndexes()
437 item = self.history_list.currentItem()
439 tx_hash = str(item.data(0, Qt.UserRole).toString())
440 if not tx_hash: return
442 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
443 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
444 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
445 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
448 def tx_details(self, tx_hash):
449 tx_details = self.wallet.get_tx_details(tx_hash)
450 QMessageBox.information(self, 'Details', tx_details, 'OK')
453 def tx_label_clicked(self, item, column):
454 if column==2 and item.isSelected():
455 tx_hash = str(item.toolTip(0))
457 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
458 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
459 self.history_list.editItem( item, column )
460 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
463 def tx_label_changed(self, item, column):
467 tx_hash = str(item.toolTip(0))
468 tx = self.wallet.transactions.get(tx_hash)
469 s = self.wallet.labels.get(tx_hash)
470 text = unicode( item.text(2) )
472 self.wallet.labels[tx_hash] = text
473 item.setForeground(2, QBrush(QColor('black')))
475 if s: self.wallet.labels.pop(tx_hash)
476 text = self.wallet.get_default_label(tx_hash)
477 item.setText(2, text)
478 item.setForeground(2, QBrush(QColor('gray')))
482 def edit_label(self, is_recv):
483 l = self.receive_list if is_recv else self.contacts_list
484 c = 2 if is_recv else 1
485 item = l.currentItem()
486 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
487 l.editItem( item, c )
488 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
490 def edit_amount(self):
491 l = self.receive_list
492 item = l.currentItem()
493 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
494 l.editItem( item, 3 )
495 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
498 def address_label_clicked(self, item, column, l, column_addr, column_label):
499 if column == column_label and item.isSelected():
500 addr = unicode( item.text(column_addr) )
501 label = unicode( item.text(column_label) )
502 if label in self.wallet.aliases.keys():
504 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
505 l.editItem( item, column )
506 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
509 def address_label_changed(self, item, column, l, column_addr, column_label):
511 if column == column_label:
512 addr = unicode( item.text(column_addr) )
513 text = unicode( item.text(column_label) )
517 if text not in self.wallet.aliases.keys():
518 old_addr = self.wallet.labels.get(text)
520 self.wallet.labels[addr] = text
523 print_error("Error: This is one of your aliases")
524 label = self.wallet.labels.get(addr,'')
525 item.setText(column_label, QString(label))
527 s = self.wallet.labels.get(addr)
529 self.wallet.labels.pop(addr)
533 self.update_history_tab()
534 self.update_completions()
536 self.recv_changed(item)
539 address = unicode( item.text(column_addr) )
540 text = unicode( item.text(3) )
542 index = self.wallet.addresses.index(address)
547 amount = int( Decimal(text) * 100000000 )
548 item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
550 amount = self.wallet.requested_amounts.get(address)
552 item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
557 self.wallet.requested_amounts[address] = amount
559 label = self.wallet.labels.get(address)
561 label = self.merchant_name + ' - %04d'%(index+1)
562 self.wallet.labels[address] = label
564 self.update_receive_item(self.receive_list.currentItem())
566 self.qr_window.set_content( address, label, amount )
569 def recv_changed(self, a):
570 "current item changed"
571 if a is not None and self.qr_window and self.qr_window.isVisible():
572 address = str(a.text(1))
573 label = self.wallet.labels.get(address)
574 amount = self.wallet.requested_amounts.get(address)
575 self.qr_window.set_content( address, label, amount )
578 def update_history_tab(self):
580 self.history_list.clear()
581 for item in self.wallet.get_tx_history():
582 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
585 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
591 icon = QIcon(":icons/unconfirmed.png")
593 icon = QIcon(":icons/clock%d.png"%conf)
595 icon = QIcon(":icons/confirmed.png")
598 icon = QIcon(":icons/unconfirmed.png")
600 if value is not None:
601 v_str = format_satoshis(value, True, self.wallet.num_zeros)
605 balance_str = format_satoshis(balance, False, self.wallet.num_zeros)
608 label, is_default_label = self.wallet.get_label(tx_hash)
610 label = _('Pruned transaction outputs')
611 is_default_label = False
613 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
614 item.setFont(2, QFont(MONOSPACE_FONT))
615 item.setFont(3, QFont(MONOSPACE_FONT))
616 item.setFont(4, QFont(MONOSPACE_FONT))
618 item.setForeground(3, QBrush(QColor("#BC1E1E")))
620 item.setData(0, Qt.UserRole, tx_hash)
621 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
623 item.setForeground(2, QBrush(QColor('grey')))
625 item.setIcon(0, icon)
626 self.history_list.insertTopLevelItem(0,item)
629 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
632 def create_send_tab(self):
637 grid.setColumnMinimumWidth(3,300)
638 grid.setColumnStretch(5,1)
640 self.payto_e = QLineEdit()
641 grid.addWidget(QLabel(_('Pay to')), 1, 0)
642 grid.addWidget(self.payto_e, 1, 1, 1, 3)
645 qrcode = qrscanner.scan_qr()
646 if 'address' in qrcode:
647 self.payto_e.setText(qrcode['address'])
648 if 'amount' in qrcode:
649 self.amount_e.setText(str(qrcode['amount']))
650 if 'label' in qrcode:
651 self.message_e.setText(qrcode['label'])
652 if 'message' in qrcode:
653 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
656 if qrscanner.is_available():
657 b = QPushButton(_("Scan QR code"))
658 b.clicked.connect(fill_from_qr)
659 grid.addWidget(b, 1, 5)
661 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)
663 completer = QCompleter()
664 completer.setCaseSensitivity(False)
665 self.payto_e.setCompleter(completer)
666 completer.setModel(self.completions)
668 self.message_e = QLineEdit()
669 grid.addWidget(QLabel(_('Description')), 2, 0)
670 grid.addWidget(self.message_e, 2, 1, 1, 3)
671 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)
673 self.amount_e = QLineEdit()
674 grid.addWidget(QLabel(_('Amount')), 3, 0)
675 grid.addWidget(self.amount_e, 3, 1, 1, 2)
676 grid.addWidget(HelpButton(
677 _('Amount to be sent.') + '\n\n' \
678 + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.')), 3, 3)
680 self.fee_e = QLineEdit()
681 grid.addWidget(QLabel(_('Fee')), 4, 0)
682 grid.addWidget(self.fee_e, 4, 1, 1, 2)
683 grid.addWidget(HelpButton(
684 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
685 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
686 + _('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)
688 b = EnterButton(_("Send"), self.do_send)
689 grid.addWidget(b, 6, 1)
691 b = EnterButton(_("Clear"),self.do_clear)
692 grid.addWidget(b, 6, 2)
694 self.payto_sig = QLabel('')
695 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
697 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
698 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
707 def entry_changed( is_fee ):
708 self.funds_error = False
709 amount = numbify(self.amount_e)
710 fee = numbify(self.fee_e)
711 if not is_fee: fee = None
714 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
716 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
719 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
720 text = self.status_text
723 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
724 self.funds_error = True
725 text = _( "Not enough funds" )
727 self.statusBar().showMessage(text)
728 self.amount_e.setPalette(palette)
729 self.fee_e.setPalette(palette)
731 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
732 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
737 def update_completions(self):
739 for addr,label in self.wallet.labels.items():
740 if addr in self.wallet.addressbook:
741 l.append( label + ' <' + addr + '>')
742 l = l + self.wallet.aliases.keys()
744 self.completions.setStringList(l)
750 label = unicode( self.message_e.text() )
751 r = unicode( self.payto_e.text() )
755 m1 = re.match(ALIAS_REGEXP, r)
756 # label or alias, with address in brackets
757 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
760 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
764 to_address = m2.group(2)
768 if not self.wallet.is_valid(to_address):
769 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
773 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
775 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
778 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
780 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
783 if self.wallet.use_encryption:
784 password = self.password_dialog()
791 tx = self.wallet.mktx( [(to_address, amount)], label, password, fee)
792 except BaseException, e:
793 self.show_message(str(e))
797 h = self.wallet.send_tx(tx)
798 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
799 status, msg = self.wallet.receive_tx( h )
801 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
803 self.update_contacts_tab()
805 QMessageBox.warning(self, _('Error'), msg, _('OK'))
807 filename = 'unsigned_tx'
808 f = open(filename,'w')
811 QMessageBox.information(self, _('Unsigned transaction'), _("Unsigned transaction was saved to file:") + " " +filename, _('OK'))
814 def set_url(self, url):
815 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
816 self.tabs.setCurrentIndex(1)
817 label = self.wallet.labels.get(payto)
818 m_addr = label + ' <'+ payto+'>' if label else payto
819 self.payto_e.setText(m_addr)
821 self.message_e.setText(message)
822 self.amount_e.setText(amount)
824 self.set_frozen(self.payto_e,True)
825 self.set_frozen(self.amount_e,True)
826 self.set_frozen(self.message_e,True)
827 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
829 self.payto_sig.setVisible(False)
832 self.payto_sig.setVisible(False)
833 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
835 self.set_frozen(e,False)
837 def set_frozen(self,entry,frozen):
839 entry.setReadOnly(True)
840 entry.setFrame(False)
842 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
843 entry.setPalette(palette)
845 entry.setReadOnly(False)
848 palette.setColor(entry.backgroundRole(), QColor('white'))
849 entry.setPalette(palette)
852 def toggle_freeze(self,addr):
854 if addr in self.wallet.frozen_addresses:
855 self.wallet.unfreeze(addr)
857 self.wallet.freeze(addr)
858 self.update_receive_tab()
860 def toggle_priority(self,addr):
862 if addr in self.wallet.prioritized_addresses:
863 self.wallet.unprioritize(addr)
865 self.wallet.prioritize(addr)
866 self.update_receive_tab()
869 def create_list_tab(self, headers):
870 "generic tab creation method"
871 l = MyTreeWidget(self)
872 l.setColumnCount( len(headers) )
873 l.setHeaderLabels( headers )
883 vbox.addWidget(buttons)
888 buttons.setLayout(hbox)
893 def create_receive_tab(self):
894 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Requested'), _('Balance'), _('Tx')])
895 l.setContextMenuPolicy(Qt.CustomContextMenu)
896 l.customContextMenuRequested.connect(self.create_receive_menu)
897 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
898 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
899 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.recv_changed(a))
900 self.receive_list = l
901 self.receive_buttons_hbox = hbox
907 def receive_tab_set_mode(self, i):
908 self.receive_tab_mode = i
909 self.config.set_key('qt_receive_tab_mode', self.receive_tab_mode, True)
911 self.update_receive_tab()
912 self.toggle_QR_window(self.receive_tab_mode == 2)
915 def create_contacts_tab(self):
916 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
917 l.setContextMenuPolicy(Qt.CustomContextMenu)
918 l.customContextMenuRequested.connect(self.create_contact_menu)
919 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
920 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
921 self.contacts_list = l
922 self.contacts_buttons_hbox = hbox
923 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
928 def delete_imported_key(self, addr):
929 if self.question("Do you want to remove %s from your wallet?"%addr):
930 self.wallet.imported_keys.pop(addr)
931 self.update_receive_tab()
932 self.update_history_tab()
936 def create_receive_menu(self, position):
937 # fixme: this function apparently has a side effect.
938 # if it is not called the menu pops up several times
939 #self.receive_list.selectedIndexes()
941 item = self.receive_list.itemAt(position)
943 addr = unicode(item.text(1))
945 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
946 if self.receive_tab_mode == 2:
947 menu.addAction(_("Request amount"), lambda: self.edit_amount())
948 menu.addAction(_("View QR"), lambda: ElectrumWindow.show_qrcode("Address","bitcoin:"+addr) )
949 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
950 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
951 if addr in self.wallet.imported_keys:
952 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
954 if self.receive_tab_mode == 1:
955 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
956 menu.addAction(t, lambda: self.toggle_freeze(addr))
957 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
958 menu.addAction(t, lambda: self.toggle_priority(addr))
960 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
963 def payto(self, x, is_alias):
970 label = self.wallet.labels.get(addr)
971 m_addr = label + ' <' + addr + '>' if label else addr
972 self.tabs.setCurrentIndex(1)
973 self.payto_e.setText(m_addr)
974 self.amount_e.setFocus()
976 def delete_contact(self, x, is_alias):
977 if self.question("Do you want to remove %s from your list of contacts?"%x):
978 if not is_alias and x in self.wallet.addressbook:
979 self.wallet.addressbook.remove(x)
980 if x in self.wallet.labels.keys():
981 self.wallet.labels.pop(x)
982 elif is_alias and x in self.wallet.aliases:
983 self.wallet.aliases.pop(x)
984 self.update_history_tab()
985 self.update_contacts_tab()
986 self.update_completions()
988 def create_contact_menu(self, position):
989 # fixme: this function apparently has a side effect.
990 # if it is not called the menu pops up several times
991 #self.contacts_list.selectedIndexes()
993 item = self.contacts_list.itemAt(position)
995 addr = unicode(item.text(0))
996 label = unicode(item.text(1))
997 is_alias = label in self.wallet.aliases.keys()
998 x = label if is_alias else addr
1000 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
1001 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
1002 menu.addAction(_("View QR code"),lambda: self.show_qrcode("Address","bitcoin:"+addr))
1004 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1006 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
1007 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
1008 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1011 def update_receive_item(self, item):
1012 address = str( item.data(1,0).toString() )
1014 flags = self.wallet.get_address_flags(address)
1015 item.setData(0,0,flags)
1017 label = self.wallet.labels.get(address,'')
1018 item.setData(2,0,label)
1020 amount = self.wallet.requested_amounts.get(address,None)
1021 amount_str = format_satoshis( amount, False, self.wallet.num_zeros ) if amount is not None else ""
1022 item.setData(3,0,amount_str)
1024 c, u = self.wallet.get_addr_balance(address)
1025 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1026 item.setData(4,0,balance)
1028 if self.receive_tab_mode == 1:
1029 if address in self.wallet.frozen_addresses:
1030 item.setBackgroundColor(1, QColor('lightblue'))
1031 elif address in self.wallet.prioritized_addresses:
1032 item.setBackgroundColor(1, QColor('lightgreen'))
1035 def update_receive_tab(self):
1036 l = self.receive_list
1039 l.setColumnHidden(0, not self.receive_tab_mode == 1)
1040 l.setColumnHidden(3, not self.receive_tab_mode == 2)
1041 l.setColumnHidden(4, self.receive_tab_mode == 0)
1042 l.setColumnHidden(5, not self.receive_tab_mode == 1)
1043 l.setColumnWidth(0, 50)
1044 l.setColumnWidth(1, 310)
1045 l.setColumnWidth(2, 200)
1046 l.setColumnWidth(3, 130)
1047 l.setColumnWidth(4, 130)
1048 l.setColumnWidth(5, 10)
1052 for address in self.wallet.all_addresses():
1054 if self.wallet.is_change(address) and self.receive_tab_mode != 1:
1058 h = self.wallet.history.get(address,[])
1061 for tx_hash, tx_height in h:
1062 tx = self.wallet.transactions.get(tx_hash)
1070 if address in self.wallet.addresses:
1072 if gap > self.wallet.gap_limit:
1075 if address in self.wallet.addresses:
1078 item = QTreeWidgetItem( [ '', address, '', '', '', num_tx] )
1079 item.setFont(0, QFont(MONOSPACE_FONT))
1080 item.setFont(1, QFont(MONOSPACE_FONT))
1081 item.setFont(3, QFont(MONOSPACE_FONT))
1082 self.update_receive_item(item)
1083 if is_red and address in self.wallet.addresses:
1084 item.setBackgroundColor(1, QColor('red'))
1085 l.addTopLevelItem(item)
1087 # we use column 1 because column 0 may be hidden
1088 l.setCurrentItem(l.topLevelItem(0),1)
1090 def show_contact_details(self, m):
1091 a = self.wallet.aliases.get(m)
1093 if a[0] in self.wallet.authorities.keys():
1094 s = self.wallet.authorities.get(a[0])
1097 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
1098 QMessageBox.information(self, 'Alias', msg, 'OK')
1100 def update_contacts_tab(self):
1102 l = self.contacts_list
1104 l.setColumnWidth(0, 350)
1105 l.setColumnWidth(1, 330)
1106 l.setColumnWidth(2, 100)
1109 for alias, v in self.wallet.aliases.items():
1111 alias_targets.append(target)
1112 item = QTreeWidgetItem( [ target, alias, '-'] )
1113 item.setBackgroundColor(0, QColor('lightgray'))
1114 l.addTopLevelItem(item)
1116 for address in self.wallet.addressbook:
1117 if address in alias_targets: continue
1118 label = self.wallet.labels.get(address,'')
1120 for item in self.wallet.transactions.values():
1121 if address in item['outputs'] : n=n+1
1123 item = QTreeWidgetItem( [ address, label, tx] )
1124 item.setFont(0, QFont(MONOSPACE_FONT))
1125 l.addTopLevelItem(item)
1127 l.setCurrentItem(l.topLevelItem(0))
1129 def create_wall_tab(self):
1130 self.textbox = textbox = QTextEdit(self)
1131 textbox.setFont(QFont(MONOSPACE_FONT))
1132 textbox.setReadOnly(True)
1135 def create_status_bar(self):
1136 self.status_text = ""
1138 sb.setFixedHeight(35)
1139 qtVersion = qVersion()
1140 if (int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
1141 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), "Switch to Lite Mode", self.go_lite ) )
1142 if self.wallet.seed:
1143 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
1144 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
1145 if self.wallet.seed:
1146 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
1147 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
1148 sb.addPermanentWidget( self.status_button )
1149 self.setStatusBar(sb)
1155 self.lite.mini.show()
1157 self.lite = gui_lite.ElectrumGui(self.wallet, self.config, self)
1158 self.lite.main(None)
1160 def new_contact_dialog(self):
1161 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1162 address = unicode(text)
1164 if self.wallet.is_valid(address):
1165 self.wallet.addressbook.append(address)
1167 self.update_contacts_tab()
1168 self.update_history_tab()
1169 self.update_completions()
1171 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1174 def show_seed_dialog(wallet, parent=None):
1176 QMessageBox.information(parent, _('Message'),
1177 _('No seed'), _('OK'))
1180 if wallet.use_encryption:
1181 password = parent.password_dialog()
1188 seed = wallet.decode_seed(password)
1190 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1193 dialog = QDialog(None)
1195 dialog.setWindowTitle("Electrum")
1197 brainwallet = ' '.join(mnemonic.mn_encode(seed))
1199 msg = _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
1200 + _("Please write down or memorize these 12 words (order is important).") + " " \
1201 + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
1202 + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
1204 main_text = QLabel(msg)
1205 main_text.setWordWrap(True)
1208 logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1215 copy_function = lambda: app.clipboard().setText(brainwallet)
1216 copy_button = QPushButton(_("Copy to Clipboard"))
1217 copy_button.clicked.connect(copy_function)
1219 show_qr_function = lambda: ElectrumWindow.show_qrcode(_("Seed"), seed)
1220 qr_button = QPushButton(_("View as QR Code"))
1221 qr_button.clicked.connect(show_qr_function)
1223 ok_button = QPushButton(_("OK"))
1224 ok_button.setDefault(True)
1225 ok_button.clicked.connect(dialog.accept)
1227 main_layout = QGridLayout()
1228 main_layout.addWidget(logo, 0, 0)
1229 main_layout.addWidget(main_text, 0, 1, 1, -1)
1230 main_layout.addWidget(copy_button, 1, 1)
1231 main_layout.addWidget(qr_button, 1, 2)
1232 main_layout.addWidget(ok_button, 1, 3)
1233 dialog.setLayout(main_layout)
1238 def show_qrcode(title, data):
1242 d.setWindowTitle(title)
1243 d.setMinimumSize(270, 300)
1244 vbox = QVBoxLayout()
1245 qrw = QRCodeWidget(data)
1246 vbox.addWidget(qrw, 1)
1247 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1248 hbox = QHBoxLayout()
1252 filename = "qrcode.bmp"
1253 bmp.save_qrcode(qrw.qr, filename)
1254 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1256 b = QPushButton(_("Print"))
1258 b.clicked.connect(print_qr)
1260 b = QPushButton(_("Close"))
1262 b.clicked.connect(d.accept)
1264 vbox.addLayout(hbox)
1268 def sign_message(self,address):
1269 if not address: return
1272 d.setWindowTitle('Sign Message')
1273 d.setMinimumSize(410, 290)
1275 tab_widget = QTabWidget()
1277 layout = QGridLayout(tab)
1279 sign_address = QLineEdit()
1281 sign_address.setText(address)
1282 layout.addWidget(QLabel(_('Address')), 1, 0)
1283 layout.addWidget(sign_address, 1, 1)
1285 sign_message = QTextEdit()
1286 layout.addWidget(QLabel(_('Message')), 2, 0)
1287 layout.addWidget(sign_message, 2, 1)
1288 layout.setRowStretch(2,3)
1290 sign_signature = QTextEdit()
1291 layout.addWidget(QLabel(_('Signature')), 3, 0)
1292 layout.addWidget(sign_signature, 3, 1)
1293 layout.setRowStretch(3,1)
1296 if self.wallet.use_encryption:
1297 password = self.password_dialog()
1304 signature = self.wallet.sign_message(str(sign_address.text()), str(sign_message.toPlainText()), password)
1305 sign_signature.setText(signature)
1306 except BaseException, e:
1307 self.show_message(str(e))
1310 hbox = QHBoxLayout()
1311 b = QPushButton(_("Sign"))
1313 b.clicked.connect(do_sign)
1314 b = QPushButton(_("Close"))
1315 b.clicked.connect(d.accept)
1317 layout.addLayout(hbox, 4, 1)
1318 tab_widget.addTab(tab, "Sign")
1322 layout = QGridLayout(tab)
1324 verify_address = QLineEdit()
1325 layout.addWidget(QLabel(_('Address')), 1, 0)
1326 layout.addWidget(verify_address, 1, 1)
1328 verify_message = QTextEdit()
1329 layout.addWidget(QLabel(_('Message')), 2, 0)
1330 layout.addWidget(verify_message, 2, 1)
1331 layout.setRowStretch(2,3)
1333 verify_signature = QTextEdit()
1334 layout.addWidget(QLabel(_('Signature')), 3, 0)
1335 layout.addWidget(verify_signature, 3, 1)
1336 layout.setRowStretch(3,1)
1340 self.wallet.verify_message(verify_address.text(), str(verify_signature.toPlainText()), str(verify_message.toPlainText()))
1341 self.show_message("Signature verified")
1342 except BaseException, e:
1343 self.show_message(str(e))
1346 hbox = QHBoxLayout()
1347 b = QPushButton(_("Verify"))
1348 b.clicked.connect(do_verify)
1350 b = QPushButton(_("Close"))
1351 b.clicked.connect(d.accept)
1353 layout.addLayout(hbox, 4, 1)
1354 tab_widget.addTab(tab, "Verify")
1356 vbox = QVBoxLayout()
1357 vbox.addWidget(tab_widget)
1362 def toggle_QR_window(self, show):
1363 if show and not self.qr_window:
1364 self.qr_window = QR_Window()
1365 self.qr_window.setVisible(True)
1366 self.qr_window_geometry = self.qr_window.geometry()
1367 item = self.receive_list.currentItem()
1369 address = str(item.text(1))
1370 label = self.wallet.labels.get(address)
1371 amount = self.wallet.requested_amounts.get(address)
1372 self.qr_window.set_content( address, label, amount )
1374 elif show and self.qr_window and not self.qr_window.isVisible():
1375 self.qr_window.setVisible(True)
1376 self.qr_window.setGeometry(self.qr_window_geometry)
1378 elif not show and self.qr_window and self.qr_window.isVisible():
1379 self.qr_window_geometry = self.qr_window.geometry()
1380 self.qr_window.setVisible(False)
1382 #self.print_button.setHidden(self.qr_window is None or not self.qr_window.isVisible())
1383 self.receive_list.setColumnHidden(3, self.qr_window is None or not self.qr_window.isVisible())
1384 self.receive_list.setColumnWidth(2, 200)
1387 def question(self, msg):
1388 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1390 def show_message(self, msg):
1391 QMessageBox.information(self, _('Message'), msg, _('OK'))
1393 def password_dialog(self ):
1400 vbox = QVBoxLayout()
1401 msg = _('Please enter your password')
1402 vbox.addWidget(QLabel(msg))
1404 grid = QGridLayout()
1406 grid.addWidget(QLabel(_('Password')), 1, 0)
1407 grid.addWidget(pw, 1, 1)
1408 vbox.addLayout(grid)
1410 vbox.addLayout(ok_cancel_buttons(d))
1413 if not d.exec_(): return
1414 return unicode(pw.text())
1421 def change_password_dialog( wallet, parent=None ):
1424 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1432 new_pw = QLineEdit()
1433 new_pw.setEchoMode(2)
1434 conf_pw = QLineEdit()
1435 conf_pw.setEchoMode(2)
1437 vbox = QVBoxLayout()
1439 msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1440 +_('To disable wallet encryption, enter an empty new password.')) \
1441 if wallet.use_encryption else _('Your wallet keys are not encrypted')
1443 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1444 +_("Leave these fields empty if you want to disable encryption.")
1445 vbox.addWidget(QLabel(msg))
1447 grid = QGridLayout()
1450 if wallet.use_encryption:
1451 grid.addWidget(QLabel(_('Password')), 1, 0)
1452 grid.addWidget(pw, 1, 1)
1454 grid.addWidget(QLabel(_('New Password')), 2, 0)
1455 grid.addWidget(new_pw, 2, 1)
1457 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1458 grid.addWidget(conf_pw, 3, 1)
1459 vbox.addLayout(grid)
1461 vbox.addLayout(ok_cancel_buttons(d))
1464 if not d.exec_(): return
1466 password = unicode(pw.text()) if wallet.use_encryption else None
1467 new_password = unicode(new_pw.text())
1468 new_password2 = unicode(conf_pw.text())
1471 seed = wallet.decode_seed(password)
1473 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1476 if new_password != new_password2:
1477 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1478 return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1480 wallet.update_password(seed, password, new_password)
1483 def seed_dialog(wallet, parent=None):
1487 vbox = QVBoxLayout()
1488 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1489 vbox.addWidget(QLabel(msg))
1491 grid = QGridLayout()
1494 seed_e = QLineEdit()
1495 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1496 grid.addWidget(seed_e, 1, 1)
1500 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1501 grid.addWidget(gap_e, 2, 1)
1502 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1503 vbox.addLayout(grid)
1505 vbox.addLayout(ok_cancel_buttons(d))
1508 if not d.exec_(): return
1511 gap = int(unicode(gap_e.text()))
1513 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1517 seed = unicode(seed_e.text())
1520 print_error("Warning: Not hex, trying decode")
1522 seed = mnemonic.mn_decode( seed.split(' ') )
1524 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1527 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1530 wallet.seed = str(seed)
1531 #print repr(wallet.seed)
1532 wallet.gap_limit = gap
1536 def do_import_labels(self):
1537 labelsFile = QFileDialog.getOpenFileName(QWidget(), "Open text file", util.user_dir(), self.tr("Text Files (labels.dat)"))
1538 if not labelsFile: return
1540 f = open(labelsFile, 'r')
1543 self.wallet.labels = json.loads(data)
1545 QMessageBox.information(None, "Labels imported", "Your labels where imported from '%s'" % str(labelsFile))
1546 except (IOError, os.error), reason:
1547 QMessageBox.critical(None, "Unable to export labels", "Electrum was unable to export your labels.\n" + str(reason))
1550 def do_export_labels(self):
1551 labels = self.wallet.labels
1553 labelsFile = util.user_dir() + '/labels.dat'
1554 f = open(labelsFile, 'w+')
1555 json.dump(labels, f)
1557 QMessageBox.information(None, "Labels exported", "Your labels where exported to '%s'" % str(labelsFile))
1558 except (IOError, os.error), reason:
1559 QMessageBox.critical(None, "Unable to export labels", "Electrum was unable to export your labels.\n" + str(reason))
1561 def do_export_history(self):
1562 from gui_lite import csv_transaction
1563 csv_transaction(self.wallet)
1565 def do_import_privkey(self):
1566 if not self.wallet.imported_keys:
1567 r = QMessageBox.question(None, _('Warning'), _('Warning: Imported keys are not recoverable from seed.') + ' ' \
1568 + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '\n\n' \
1569 + _('Are you sure you understand what you are doing?'), 3, 4)
1572 text, ok = QInputDialog.getText(self, _('Import private key'), _('Private Key') + ':')
1575 if self.wallet.use_encryption:
1576 password = self.password_dialog()
1582 addr = self.wallet.import_key(sec, password)
1584 QMessageBox.critical(None, "Unable to import key", "error")
1586 QMessageBox.information(None, "Key imported", addr)
1587 self.update_receive_tab()
1588 self.update_history_tab()
1589 except BaseException as e:
1590 QMessageBox.critical(None, "Unable to import key", str(e))
1592 def settings_dialog(self):
1594 d.setWindowTitle(_('Electrum Settings'))
1596 vbox = QVBoxLayout()
1598 tabs = QTabWidget(self)
1599 vbox.addWidget(tabs)
1602 grid_ui = QGridLayout(tab1)
1603 grid_ui.setColumnStretch(0,1)
1604 tabs.addTab(tab1, _('Display') )
1606 nz_label = QLabel(_('Display zeros'))
1607 grid_ui.addWidget(nz_label, 3, 0)
1609 nz_e.setText("%d"% self.wallet.num_zeros)
1610 grid_ui.addWidget(nz_e, 3, 1)
1611 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1612 grid_ui.addWidget(HelpButton(msg), 3, 2)
1613 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1614 if not self.config.is_modifiable('num_zeros'):
1615 for w in [nz_e, nz_label]: w.setEnabled(False)
1617 gui_label=QLabel(_('Default GUI') + ':')
1618 grid_ui.addWidget(gui_label , 7, 0)
1619 gui_combo = QComboBox()
1620 gui_combo.addItems(['Lite', 'Classic'])
1621 index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1622 if index==-1: index = 1
1623 gui_combo.setCurrentIndex(index)
1624 grid_ui.addWidget(gui_combo, 7, 1)
1625 grid_ui.addWidget(HelpButton(_('Select which GUI mode to use at start up.'+'\n'+'Note: use the command line to access the "text" and "gtk" GUIs')), 7, 2)
1626 if not self.config.is_modifiable('gui'):
1627 for w in [gui_combo, gui_label]: w.setEnabled(False)
1629 lang_label=QLabel(_('Language') + ':')
1630 grid_ui.addWidget(lang_label , 8, 0)
1631 lang_combo = QComboBox()
1632 from i18n import languages
1633 lang_combo.addItems(languages.values())
1635 index = languages.keys().index(self.config.get("language",''))
1638 lang_combo.setCurrentIndex(index)
1639 grid_ui.addWidget(lang_combo, 8, 1)
1640 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart). ')), 8, 2)
1641 if not self.config.is_modifiable('language'):
1642 for w in [lang_combo, lang_label]: w.setEnabled(False)
1644 currencies = self.exchanger.get_currencies()
1645 currencies.insert(0, "None")
1647 cur_label=QLabel(_('Currency') + ':')
1648 grid_ui.addWidget(cur_label , 9, 0)
1649 cur_combo = QComboBox()
1650 cur_combo.addItems(currencies)
1652 index = currencies.index(self.config.get('currency', "None"))
1655 cur_combo.setCurrentIndex(index)
1656 grid_ui.addWidget(cur_combo, 9, 1)
1657 grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes. ')), 9, 2)
1659 view_label=QLabel(_('Receive Tab') + ':')
1660 grid_ui.addWidget(view_label , 10, 0)
1661 view_combo = QComboBox()
1662 view_combo.addItems([_('Simple'), _('Advanced'), _('Point of Sale')])
1663 view_combo.setCurrentIndex(self.receive_tab_mode)
1664 grid_ui.addWidget(view_combo, 10, 1)
1665 hh = _('This selects the interaction mode of the "Receive" tab. ') + '\n\n' \
1666 + _('Simple') + ': ' + _('Show only addresses and labels.') + '\n\n' \
1667 + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' \
1668 + _('Point of Sale') + ': ' + _('Show QR code window and amounts requested for each address. Add menu item to request amount.') + '\n\n'
1670 grid_ui.addWidget(HelpButton(hh), 10, 2)
1674 grid_wallet = QGridLayout(tab2)
1675 grid_wallet.setColumnStretch(0,1)
1676 tabs.addTab(tab2, _('Wallet') )
1678 fee_label = QLabel(_('Transaction fee'))
1679 grid_wallet.addWidget(fee_label, 0, 0)
1681 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1682 grid_wallet.addWidget(fee_e, 0, 1)
1683 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1684 + _('Recommended value') + ': 0.001'
1685 grid_wallet.addWidget(HelpButton(msg), 0, 2)
1686 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1687 if not self.config.is_modifiable('fee'):
1688 for w in [fee_e, fee_label]: w.setEnabled(False)
1690 usechange_label = QLabel(_('Use change addresses'))
1691 grid_wallet.addWidget(usechange_label, 1, 0)
1692 usechange_combo = QComboBox()
1693 usechange_combo.addItems(['Yes', 'No'])
1694 usechange_combo.setCurrentIndex(not self.wallet.use_change)
1695 grid_wallet.addWidget(usechange_combo, 1, 1)
1696 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 1, 2)
1697 if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1699 gap_label = QLabel(_('Gap limit'))
1700 grid_wallet.addWidget(gap_label, 2, 0)
1702 gap_e.setText("%d"% self.wallet.gap_limit)
1703 grid_wallet.addWidget(gap_e, 2, 1)
1704 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1705 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1706 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1707 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1708 + _('Warning') + ': ' \
1709 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1710 + _('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'
1711 grid_wallet.addWidget(HelpButton(msg), 2, 2)
1712 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1713 if not self.config.is_modifiable('gap_limit'):
1714 for w in [gap_e, gap_label]: w.setEnabled(False)
1716 grid_wallet.setRowStretch(3,1)
1721 grid_io = QGridLayout(tab3)
1722 grid_io.setColumnStretch(0,1)
1723 tabs.addTab(tab3, _('Import/Export') )
1725 grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1726 grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1727 grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1728 grid_io.addWidget(HelpButton('Export your labels as json'), 1, 3)
1730 grid_io.addWidget(QLabel(_('History')), 2, 0)
1731 grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1732 grid_io.addWidget(HelpButton('Export your transaction history as csv'), 2, 3)
1734 grid_io.addWidget(QLabel(_('Private key')), 3, 0)
1735 grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
1736 grid_io.addWidget(HelpButton('Import private key'), 3, 3)
1738 grid_io.setRowStretch(4,1)
1739 vbox.addLayout(ok_cancel_buttons(d))
1743 if not d.exec_(): return
1745 fee = unicode(fee_e.text())
1747 fee = int( 100000000 * Decimal(fee) )
1749 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1752 if self.wallet.fee != fee:
1753 self.wallet.fee = fee
1756 nz = unicode(nz_e.text())
1761 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1764 if self.wallet.num_zeros != nz:
1765 self.wallet.num_zeros = nz
1766 self.config.set_key('num_zeros', nz, True)
1767 self.update_history_tab()
1768 self.update_receive_tab()
1770 usechange_result = usechange_combo.currentIndex() == 0
1771 if self.wallet.use_change != usechange_result:
1772 self.wallet.use_change = usechange_result
1773 self.config.set_key('use_change', self.wallet.use_change, True)
1776 n = int(gap_e.text())
1778 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1781 if self.wallet.gap_limit != n:
1782 r = self.wallet.change_gap_limit(n)
1784 self.update_receive_tab()
1785 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1787 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1789 need_restart = False
1791 gui_request = str(gui_combo.currentText()).lower()
1792 if gui_request != self.config.get('gui'):
1793 self.config.set_key('gui', gui_request, True)
1796 lang_request = languages.keys()[lang_combo.currentIndex()]
1797 if lang_request != self.config.get('language'):
1798 self.config.set_key("language", lang_request, True)
1801 cur_request = str(currencies[cur_combo.currentIndex()])
1802 if cur_request != self.config.get('currency', "None"):
1803 self.config.set_key('currency', cur_request, True)
1804 self.update_wallet()
1807 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1809 self.receive_tab_set_mode(view_combo.currentIndex())
1813 def network_dialog(wallet, parent=None):
1814 interface = wallet.interface
1816 if interface.is_connected:
1817 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1819 status = _("Not connected")
1820 server = interface.server
1823 status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
1824 server = interface.server
1826 plist, servers_list = interface.get_servers_list()
1830 d.setWindowTitle(_('Server'))
1831 d.setMinimumSize(375, 20)
1833 vbox = QVBoxLayout()
1836 hbox = QHBoxLayout()
1838 l.setPixmap(QPixmap(":icons/network.png"))
1841 hbox.addWidget(QLabel(status))
1843 vbox.addLayout(hbox)
1847 grid = QGridLayout()
1849 vbox.addLayout(grid)
1852 server_protocol = QComboBox()
1853 server_host = QLineEdit()
1854 server_host.setFixedWidth(200)
1855 server_port = QLineEdit()
1856 server_port.setFixedWidth(60)
1858 protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1859 protocol_letters = 'thsg'
1860 DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1861 server_protocol.addItems(protocol_names)
1863 grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1864 grid.addWidget(server_protocol, 0, 1)
1865 grid.addWidget(server_host, 0, 2)
1866 grid.addWidget(server_port, 0, 3)
1868 def change_protocol(p):
1869 protocol = protocol_letters[p]
1870 host = unicode(server_host.text())
1871 pp = plist.get(host,DEFAULT_PORTS)
1872 if protocol not in pp.keys():
1873 protocol = pp.keys()[0]
1875 server_host.setText( host )
1876 server_port.setText( port )
1878 server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1880 label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1881 servers_list_widget = QTreeWidget(parent)
1882 servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
1883 servers_list_widget.setMaximumHeight(150)
1884 servers_list_widget.setColumnWidth(0, 240)
1885 for _host in servers_list.keys():
1886 _type = 'P' if servers_list[_host].get('pruning') else 'F'
1887 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
1889 def change_server(host, protocol=None):
1890 pp = plist.get(host,DEFAULT_PORTS)
1892 port = pp.get(protocol)
1893 if not port: protocol = None
1896 if 't' in pp.keys():
1898 port = pp.get(protocol)
1900 protocol = pp.keys()[0]
1901 port = pp.get(protocol)
1903 server_host.setText( host )
1904 server_port.setText( port )
1905 server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1907 if not plist: return
1908 for p in protocol_letters:
1909 i = protocol_letters.index(p)
1910 j = server_protocol.model().index(i,0)
1911 if p not in pp.keys():
1912 server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1914 server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1918 host, port, protocol = server.split(':')
1919 change_server(host,protocol)
1921 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1922 grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1924 if not wallet.config.is_modifiable('server'):
1925 for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1928 autocycle_cb = QCheckBox('Try random servers if disconnected')
1929 autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
1930 grid.addWidget(autocycle_cb, 3, 1, 3, 2)
1931 if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
1934 proxy_mode = QComboBox()
1935 proxy_host = QLineEdit()
1936 proxy_host.setFixedWidth(200)
1937 proxy_port = QLineEdit()
1938 proxy_port.setFixedWidth(60)
1939 proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1941 def check_for_disable(index = False):
1942 if proxy_mode.currentText() != 'NONE':
1943 proxy_host.setEnabled(True)
1944 proxy_port.setEnabled(True)
1946 proxy_host.setEnabled(False)
1947 proxy_port.setEnabled(False)
1950 proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1952 if not wallet.config.is_modifiable('proxy'):
1953 for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1955 proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1956 proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1957 proxy_host.setText(proxy_config.get("host"))
1958 proxy_port.setText(proxy_config.get("port"))
1960 grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1961 grid.addWidget(proxy_mode, 2, 1)
1962 grid.addWidget(proxy_host, 2, 2)
1963 grid.addWidget(proxy_port, 2, 3)
1966 vbox.addLayout(ok_cancel_buttons(d))
1969 if not d.exec_(): return
1971 server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1972 if proxy_mode.currentText() != 'NONE':
1973 proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1977 wallet.config.set_key("proxy", proxy, True)
1978 wallet.config.set_key("server", server, True)
1979 interface.set_server(server, proxy)
1980 wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
1983 def closeEvent(self, event):
1985 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1991 def __init__(self, wallet, config, app=None):
1992 self.wallet = wallet
1993 self.config = config
1995 self.app = QApplication(sys.argv)
1998 def restore_or_create(self):
1999 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
2000 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
2001 if r==2: return None
2002 return 'restore' if r==1 else 'create'
2004 def seed_dialog(self):
2005 return ElectrumWindow.seed_dialog( self.wallet )
2007 def network_dialog(self):
2008 return ElectrumWindow.network_dialog( self.wallet, parent=None )
2011 def show_seed(self):
2012 ElectrumWindow.show_seed_dialog(self.wallet)
2015 def password_dialog(self):
2016 ElectrumWindow.change_password_dialog(self.wallet)
2019 def restore_wallet(self):
2020 wallet = self.wallet
2021 # wait until we are connected, because the user might have selected another server
2022 if not wallet.interface.is_connected:
2023 waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
2024 waiting_dialog(waiting)
2026 waiting = lambda: False if wallet.is_up_to_date() else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
2027 %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
2029 wallet.set_up_to_date(False)
2030 wallet.interface.poke('synchronizer')
2031 waiting_dialog(waiting)
2032 if wallet.is_found():
2033 print_error( "Recovery successful" )
2035 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2042 w = ElectrumWindow(self.wallet, self.config)
2043 if url: w.set_url(url)