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)
301 self.wallet.interface.register_callback('updated', self.update_callback)
302 self.wallet.interface.register_callback('connected', self.update_callback)
303 self.wallet.interface.register_callback('disconnected', self.update_callback)
304 self.wallet.interface.register_callback('disconnecting', self.update_callback)
306 self.receive_tab_mode = config.get('qt_receive_tab_mode', 0)
307 self.merchant_name = config.get('merchant_name', 'Invoice')
309 self.qr_window = None
310 self.funds_error = False
311 self.completions = QStringListModel()
313 self.tabs = tabs = QTabWidget(self)
314 tabs.addTab(self.create_history_tab(), _('History') )
315 tabs.addTab(self.create_send_tab(), _('Send') )
316 tabs.addTab(self.create_receive_tab(), _('Receive') )
317 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
318 tabs.addTab(self.create_wall_tab(), _('Wall') )
319 tabs.setMinimumSize(600, 400)
320 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
321 self.setCentralWidget(tabs)
322 self.create_status_bar()
323 self.toggle_QR_window(self.receive_tab_mode == 2)
325 g = self.config.get("winpos-qt",[100, 100, 840, 400])
326 self.setGeometry(g[0], g[1], g[2], g[3])
327 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
328 if not self.wallet.seed: title += ' [seedless]'
329 self.setWindowTitle( title )
331 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
332 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
333 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
334 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
336 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
337 #self.connect(self, SIGNAL('editamount'), self.edit_amount)
338 self.history_list.setFocus(True)
340 self.exchanger = exchange_rate.Exchanger(self)
341 self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
343 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
344 if platform.system() == 'Windows':
345 n = 3 if self.wallet.seed else 2
346 tabs.setCurrentIndex (n)
347 tabs.setCurrentIndex (0)
350 QMainWindow.close(self)
352 self.qr_window.close()
353 self.qr_window = None
355 def connect_slots(self, sender):
356 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
357 self.previous_payto_e=''
359 def timer_actions(self):
361 self.qr_window.qrw.update_qr()
363 if self.payto_e.hasFocus():
365 r = unicode( self.payto_e.text() )
366 if r != self.previous_payto_e:
367 self.previous_payto_e = r
369 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
371 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
375 s = r + ' <' + to_address + '>'
376 self.payto_e.setText(s)
379 def update_callback(self):
380 self.emit(QtCore.SIGNAL('updatesignal'))
382 def update_wallet(self):
383 if self.wallet.interface and self.wallet.interface.is_connected:
384 if not self.wallet.up_to_date:
385 text = _( "Synchronizing..." )
386 icon = QIcon(":icons/status_waiting.png")
388 c, u = self.wallet.get_balance()
389 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
390 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
391 text += self.create_quote_text(Decimal(c+u)/100000000)
392 icon = QIcon(":icons/status_connected.png")
394 text = _( "Not connected" )
395 icon = QIcon(":icons/status_disconnected.png")
398 text = _( "Not enough funds" )
400 self.statusBar().showMessage(text)
401 self.status_button.setIcon( icon )
403 if self.wallet.up_to_date or not self.wallet.interface.is_connected:
404 self.textbox.setText( self.wallet.banner )
405 self.update_history_tab()
406 self.update_receive_tab()
407 self.update_contacts_tab()
408 self.update_completions()
410 def create_quote_text(self, btc_balance):
411 quote_currency = self.config.get("currency", "None")
412 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
413 if quote_balance is None:
416 quote_text = " (%.2f %s)" % (quote_balance, quote_currency)
419 def create_history_tab(self):
420 self.history_list = l = MyTreeWidget(self)
422 l.setColumnWidth(0, 40)
423 l.setColumnWidth(1, 140)
424 l.setColumnWidth(2, 350)
425 l.setColumnWidth(3, 140)
426 l.setColumnWidth(4, 140)
427 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
428 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
429 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
431 l.setContextMenuPolicy(Qt.CustomContextMenu)
432 l.customContextMenuRequested.connect(self.create_history_menu)
436 def create_history_menu(self, position):
437 self.history_list.selectedIndexes()
438 item = self.history_list.currentItem()
440 tx_hash = str(item.data(0, Qt.UserRole).toString())
441 if not tx_hash: return
443 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
444 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
445 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
446 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
449 def tx_details(self, tx_hash):
450 tx_details = self.wallet.get_tx_details(tx_hash)
451 QMessageBox.information(self, 'Details', tx_details, 'OK')
454 def tx_label_clicked(self, item, column):
455 if column==2 and item.isSelected():
456 tx_hash = str(item.toolTip(0))
458 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
459 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
460 self.history_list.editItem( item, column )
461 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
464 def tx_label_changed(self, item, column):
468 tx_hash = str(item.toolTip(0))
469 tx = self.wallet.transactions.get(tx_hash)
470 s = self.wallet.labels.get(tx_hash)
471 text = unicode( item.text(2) )
473 self.wallet.labels[tx_hash] = text
474 item.setForeground(2, QBrush(QColor('black')))
476 if s: self.wallet.labels.pop(tx_hash)
477 text = self.wallet.get_default_label(tx_hash)
478 item.setText(2, text)
479 item.setForeground(2, QBrush(QColor('gray')))
483 def edit_label(self, is_recv):
484 l = self.receive_list if is_recv else self.contacts_list
485 c = 2 if is_recv else 1
486 item = l.currentItem()
487 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
488 l.editItem( item, c )
489 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
491 def edit_amount(self):
492 l = self.receive_list
493 item = l.currentItem()
494 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
495 l.editItem( item, 3 )
496 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
499 def address_label_clicked(self, item, column, l, column_addr, column_label):
500 if column == column_label and item.isSelected():
501 addr = unicode( item.text(column_addr) )
502 label = unicode( item.text(column_label) )
503 if label in self.wallet.aliases.keys():
505 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
506 l.editItem( item, column )
507 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
510 def address_label_changed(self, item, column, l, column_addr, column_label):
512 if column == column_label:
513 addr = unicode( item.text(column_addr) )
514 text = unicode( item.text(column_label) )
518 if text not in self.wallet.aliases.keys():
519 old_addr = self.wallet.labels.get(text)
521 self.wallet.labels[addr] = text
524 print_error("Error: This is one of your aliases")
525 label = self.wallet.labels.get(addr,'')
526 item.setText(column_label, QString(label))
528 s = self.wallet.labels.get(addr)
530 self.wallet.labels.pop(addr)
534 self.update_history_tab()
535 self.update_completions()
537 self.recv_changed(item)
540 address = unicode( item.text(column_addr) )
541 text = unicode( item.text(3) )
543 index = self.wallet.addresses.index(address)
548 amount = int( Decimal(text) * 100000000 )
549 item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
551 amount = self.wallet.requested_amounts.get(address)
553 item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
558 self.wallet.requested_amounts[address] = amount
560 label = self.wallet.labels.get(address)
562 label = self.merchant_name + ' - %04d'%(index+1)
563 self.wallet.labels[address] = label
565 self.update_receive_item(self.receive_list.currentItem())
567 self.qr_window.set_content( address, label, amount )
570 def recv_changed(self, a):
571 "current item changed"
572 if a is not None and self.qr_window and self.qr_window.isVisible():
573 address = str(a.text(1))
574 label = self.wallet.labels.get(address)
575 amount = self.wallet.requested_amounts.get(address)
576 self.qr_window.set_content( address, label, amount )
579 def update_history_tab(self):
581 self.history_list.clear()
582 for item in self.wallet.get_tx_history():
583 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
586 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
592 icon = QIcon(":icons/unconfirmed.png")
594 icon = QIcon(":icons/clock%d.png"%conf)
596 icon = QIcon(":icons/confirmed.png")
599 icon = QIcon(":icons/unconfirmed.png")
601 if value is not None:
602 v_str = format_satoshis(value, True, self.wallet.num_zeros)
606 balance_str = format_satoshis(balance, False, self.wallet.num_zeros)
609 label, is_default_label = self.wallet.get_label(tx_hash)
611 label = _('Pruned transaction outputs')
612 is_default_label = False
614 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
615 item.setFont(2, QFont(MONOSPACE_FONT))
616 item.setFont(3, QFont(MONOSPACE_FONT))
617 item.setFont(4, QFont(MONOSPACE_FONT))
619 item.setForeground(3, QBrush(QColor("#BC1E1E")))
621 item.setData(0, Qt.UserRole, tx_hash)
622 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
624 item.setForeground(2, QBrush(QColor('grey')))
626 item.setIcon(0, icon)
627 self.history_list.insertTopLevelItem(0,item)
630 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
633 def create_send_tab(self):
638 grid.setColumnMinimumWidth(3,300)
639 grid.setColumnStretch(5,1)
641 self.payto_e = QLineEdit()
642 grid.addWidget(QLabel(_('Pay to')), 1, 0)
643 grid.addWidget(self.payto_e, 1, 1, 1, 3)
646 qrcode = qrscanner.scan_qr()
647 if 'address' in qrcode:
648 self.payto_e.setText(qrcode['address'])
649 if 'amount' in qrcode:
650 self.amount_e.setText(str(qrcode['amount']))
651 if 'label' in qrcode:
652 self.message_e.setText(qrcode['label'])
653 if 'message' in qrcode:
654 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
657 if qrscanner.is_available():
658 b = QPushButton(_("Scan QR code"))
659 b.clicked.connect(fill_from_qr)
660 grid.addWidget(b, 1, 5)
662 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)
664 completer = QCompleter()
665 completer.setCaseSensitivity(False)
666 self.payto_e.setCompleter(completer)
667 completer.setModel(self.completions)
669 self.message_e = QLineEdit()
670 grid.addWidget(QLabel(_('Description')), 2, 0)
671 grid.addWidget(self.message_e, 2, 1, 1, 3)
672 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)
674 self.amount_e = QLineEdit()
675 grid.addWidget(QLabel(_('Amount')), 3, 0)
676 grid.addWidget(self.amount_e, 3, 1, 1, 2)
677 grid.addWidget(HelpButton(
678 _('Amount to be sent.') + '\n\n' \
679 + _('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)
681 self.fee_e = QLineEdit()
682 grid.addWidget(QLabel(_('Fee')), 4, 0)
683 grid.addWidget(self.fee_e, 4, 1, 1, 2)
684 grid.addWidget(HelpButton(
685 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
686 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
687 + _('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)
689 b = EnterButton(_("Send"), self.do_send)
690 grid.addWidget(b, 6, 1)
692 b = EnterButton(_("Clear"),self.do_clear)
693 grid.addWidget(b, 6, 2)
695 self.payto_sig = QLabel('')
696 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
698 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
699 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
708 def entry_changed( is_fee ):
709 self.funds_error = False
710 amount = numbify(self.amount_e)
711 fee = numbify(self.fee_e)
712 if not is_fee: fee = None
715 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
717 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
720 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
723 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
724 self.funds_error = True
725 self.amount_e.setPalette(palette)
726 self.fee_e.setPalette(palette)
729 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
730 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
735 def update_completions(self):
737 for addr,label in self.wallet.labels.items():
738 if addr in self.wallet.addressbook:
739 l.append( label + ' <' + addr + '>')
740 l = l + self.wallet.aliases.keys()
742 self.completions.setStringList(l)
748 label = unicode( self.message_e.text() )
749 r = unicode( self.payto_e.text() )
753 m1 = re.match(ALIAS_REGEXP, r)
754 # label or alias, with address in brackets
755 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
758 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
762 to_address = m2.group(2)
766 if not self.wallet.is_valid(to_address):
767 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
771 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
773 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
776 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
778 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
781 if self.wallet.use_encryption:
782 password = self.password_dialog()
789 tx = self.wallet.mktx( [(to_address, amount)], label, password, fee)
790 except BaseException, e:
791 self.show_message(str(e))
795 h = self.wallet.send_tx(tx)
796 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
797 status, msg = self.wallet.receive_tx( h )
799 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
801 self.update_contacts_tab()
803 QMessageBox.warning(self, _('Error'), msg, _('OK'))
805 filename = 'unsigned_tx'
806 f = open(filename,'w')
809 QMessageBox.information(self, _('Unsigned transaction'), _("Unsigned transaction was saved to file:") + " " +filename, _('OK'))
812 def set_url(self, url):
813 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
814 self.tabs.setCurrentIndex(1)
815 label = self.wallet.labels.get(payto)
816 m_addr = label + ' <'+ payto+'>' if label else payto
817 self.payto_e.setText(m_addr)
819 self.message_e.setText(message)
820 self.amount_e.setText(amount)
822 self.set_frozen(self.payto_e,True)
823 self.set_frozen(self.amount_e,True)
824 self.set_frozen(self.message_e,True)
825 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
827 self.payto_sig.setVisible(False)
830 self.payto_sig.setVisible(False)
831 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
833 self.set_frozen(e,False)
835 def set_frozen(self,entry,frozen):
837 entry.setReadOnly(True)
838 entry.setFrame(False)
840 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
841 entry.setPalette(palette)
843 entry.setReadOnly(False)
846 palette.setColor(entry.backgroundRole(), QColor('white'))
847 entry.setPalette(palette)
850 def toggle_freeze(self,addr):
852 if addr in self.wallet.frozen_addresses:
853 self.wallet.unfreeze(addr)
855 self.wallet.freeze(addr)
856 self.update_receive_tab()
858 def toggle_priority(self,addr):
860 if addr in self.wallet.prioritized_addresses:
861 self.wallet.unprioritize(addr)
863 self.wallet.prioritize(addr)
864 self.update_receive_tab()
867 def create_list_tab(self, headers):
868 "generic tab creation method"
869 l = MyTreeWidget(self)
870 l.setColumnCount( len(headers) )
871 l.setHeaderLabels( headers )
881 vbox.addWidget(buttons)
886 buttons.setLayout(hbox)
891 def create_receive_tab(self):
892 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Requested'), _('Balance'), _('Tx')])
893 l.setContextMenuPolicy(Qt.CustomContextMenu)
894 l.customContextMenuRequested.connect(self.create_receive_menu)
895 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
896 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
897 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.recv_changed(a))
898 self.receive_list = l
899 self.receive_buttons_hbox = hbox
905 def receive_tab_set_mode(self, i):
906 self.receive_tab_mode = i
907 self.config.set_key('qt_receive_tab_mode', self.receive_tab_mode, True)
909 self.update_receive_tab()
910 self.toggle_QR_window(self.receive_tab_mode == 2)
913 def create_contacts_tab(self):
914 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
915 l.setContextMenuPolicy(Qt.CustomContextMenu)
916 l.customContextMenuRequested.connect(self.create_contact_menu)
917 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
918 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
919 self.contacts_list = l
920 self.contacts_buttons_hbox = hbox
921 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
926 def delete_imported_key(self, addr):
927 if self.question("Do you want to remove %s from your wallet?"%addr):
928 self.wallet.imported_keys.pop(addr)
929 self.update_receive_tab()
930 self.update_history_tab()
934 def create_receive_menu(self, position):
935 # fixme: this function apparently has a side effect.
936 # if it is not called the menu pops up several times
937 #self.receive_list.selectedIndexes()
939 item = self.receive_list.itemAt(position)
941 addr = unicode(item.text(1))
943 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
944 if self.receive_tab_mode == 2:
945 menu.addAction(_("Request amount"), lambda: self.edit_amount())
946 menu.addAction(_("View QR"), lambda: ElectrumWindow.show_qrcode("Address","bitcoin:"+addr) )
947 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
948 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
949 if addr in self.wallet.imported_keys:
950 menu.addAction(_("Remove from wallet"), lambda: self.delete_imported_key(addr))
952 if self.receive_tab_mode == 1:
953 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
954 menu.addAction(t, lambda: self.toggle_freeze(addr))
955 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
956 menu.addAction(t, lambda: self.toggle_priority(addr))
958 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
961 def payto(self, x, is_alias):
968 label = self.wallet.labels.get(addr)
969 m_addr = label + ' <' + addr + '>' if label else addr
970 self.tabs.setCurrentIndex(1)
971 self.payto_e.setText(m_addr)
972 self.amount_e.setFocus()
974 def delete_contact(self, x, is_alias):
975 if self.question("Do you want to remove %s from your list of contacts?"%x):
976 if not is_alias and x in self.wallet.addressbook:
977 self.wallet.addressbook.remove(x)
978 if x in self.wallet.labels.keys():
979 self.wallet.labels.pop(x)
980 elif is_alias and x in self.wallet.aliases:
981 self.wallet.aliases.pop(x)
982 self.update_history_tab()
983 self.update_contacts_tab()
984 self.update_completions()
986 def create_contact_menu(self, position):
987 # fixme: this function apparently has a side effect.
988 # if it is not called the menu pops up several times
989 #self.contacts_list.selectedIndexes()
991 item = self.contacts_list.itemAt(position)
993 addr = unicode(item.text(0))
994 label = unicode(item.text(1))
995 is_alias = label in self.wallet.aliases.keys()
996 x = label if is_alias else addr
998 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
999 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
1000 menu.addAction(_("View QR code"),lambda: self.show_qrcode("Address","bitcoin:"+addr))
1002 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
1004 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
1005 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
1006 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
1009 def update_receive_item(self, item):
1010 address = str( item.data(1,0).toString() )
1012 flags = self.wallet.get_address_flags(address)
1013 item.setData(0,0,flags)
1015 label = self.wallet.labels.get(address,'')
1016 item.setData(2,0,label)
1018 amount = self.wallet.requested_amounts.get(address,None)
1019 amount_str = format_satoshis( amount, False, self.wallet.num_zeros ) if amount is not None else ""
1020 item.setData(3,0,amount_str)
1022 c, u = self.wallet.get_addr_balance(address)
1023 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1024 item.setData(4,0,balance)
1026 if self.receive_tab_mode == 1:
1027 if address in self.wallet.frozen_addresses:
1028 item.setBackgroundColor(1, QColor('lightblue'))
1029 elif address in self.wallet.prioritized_addresses:
1030 item.setBackgroundColor(1, QColor('lightgreen'))
1033 def update_receive_tab(self):
1034 l = self.receive_list
1037 l.setColumnHidden(0, not self.receive_tab_mode == 1)
1038 l.setColumnHidden(3, not self.receive_tab_mode == 2)
1039 l.setColumnHidden(4, self.receive_tab_mode == 0)
1040 l.setColumnHidden(5, not self.receive_tab_mode == 1)
1041 l.setColumnWidth(0, 50)
1042 l.setColumnWidth(1, 310)
1043 l.setColumnWidth(2, 200)
1044 l.setColumnWidth(3, 130)
1045 l.setColumnWidth(4, 130)
1046 l.setColumnWidth(5, 10)
1050 for address in self.wallet.all_addresses():
1052 if self.wallet.is_change(address) and self.receive_tab_mode != 1:
1056 h = self.wallet.history.get(address,[])
1059 for tx_hash, tx_height in h:
1060 tx = self.wallet.transactions.get(tx_hash)
1068 if address in self.wallet.addresses:
1070 if gap > self.wallet.gap_limit:
1073 if address in self.wallet.addresses:
1076 item = QTreeWidgetItem( [ '', address, '', '', '', num_tx] )
1077 item.setFont(0, QFont(MONOSPACE_FONT))
1078 item.setFont(1, QFont(MONOSPACE_FONT))
1079 item.setFont(3, QFont(MONOSPACE_FONT))
1080 self.update_receive_item(item)
1081 if is_red and address in self.wallet.addresses:
1082 item.setBackgroundColor(1, QColor('red'))
1083 l.addTopLevelItem(item)
1085 # we use column 1 because column 0 may be hidden
1086 l.setCurrentItem(l.topLevelItem(0),1)
1088 def show_contact_details(self, m):
1089 a = self.wallet.aliases.get(m)
1091 if a[0] in self.wallet.authorities.keys():
1092 s = self.wallet.authorities.get(a[0])
1095 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
1096 QMessageBox.information(self, 'Alias', msg, 'OK')
1098 def update_contacts_tab(self):
1100 l = self.contacts_list
1102 l.setColumnWidth(0, 350)
1103 l.setColumnWidth(1, 330)
1104 l.setColumnWidth(2, 100)
1107 for alias, v in self.wallet.aliases.items():
1109 alias_targets.append(target)
1110 item = QTreeWidgetItem( [ target, alias, '-'] )
1111 item.setBackgroundColor(0, QColor('lightgray'))
1112 l.addTopLevelItem(item)
1114 for address in self.wallet.addressbook:
1115 if address in alias_targets: continue
1116 label = self.wallet.labels.get(address,'')
1118 for item in self.wallet.transactions.values():
1119 if address in item['outputs'] : n=n+1
1121 item = QTreeWidgetItem( [ address, label, tx] )
1122 item.setFont(0, QFont(MONOSPACE_FONT))
1123 l.addTopLevelItem(item)
1125 l.setCurrentItem(l.topLevelItem(0))
1127 def create_wall_tab(self):
1128 self.textbox = textbox = QTextEdit(self)
1129 textbox.setFont(QFont(MONOSPACE_FONT))
1130 textbox.setReadOnly(True)
1133 def create_status_bar(self):
1135 sb.setFixedHeight(35)
1136 if self.wallet.seed:
1137 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
1138 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
1139 if self.wallet.seed:
1140 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
1141 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
1142 sb.addPermanentWidget( self.status_button )
1143 self.setStatusBar(sb)
1145 def new_contact_dialog(self):
1146 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1147 address = unicode(text)
1149 if self.wallet.is_valid(address):
1150 self.wallet.addressbook.append(address)
1152 self.update_contacts_tab()
1153 self.update_history_tab()
1154 self.update_completions()
1156 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1159 def show_seed_dialog(wallet, parent=None):
1161 QMessageBox.information(parent, _('Message'),
1162 _('No seed'), _('OK'))
1165 if wallet.use_encryption:
1166 password = parent.password_dialog()
1173 seed = wallet.pw_decode(wallet.seed, password)
1175 QMessageBox.warning(parent, _('Error'),
1176 _('Incorrect Password'), _('OK'))
1179 dialog = QDialog(None)
1181 dialog.setWindowTitle("Electrum")
1183 brainwallet = ' '.join(mnemonic.mn_encode(seed))
1185 msg = _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
1186 + _("Please write down or memorize these 12 words (order is important).") + " " \
1187 + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
1188 + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
1190 main_text = QLabel(msg)
1191 main_text.setWordWrap(True)
1194 logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1201 copy_function = lambda: app.clipboard().setText(brainwallet)
1202 copy_button = QPushButton(_("Copy to Clipboard"))
1203 copy_button.clicked.connect(copy_function)
1205 show_qr_function = lambda: ElectrumWindow.show_qrcode(_("Seed"), seed)
1206 qr_button = QPushButton(_("View as QR Code"))
1207 qr_button.clicked.connect(show_qr_function)
1209 ok_button = QPushButton(_("OK"))
1210 ok_button.setDefault(True)
1211 ok_button.clicked.connect(dialog.accept)
1213 main_layout = QGridLayout()
1214 main_layout.addWidget(logo, 0, 0)
1215 main_layout.addWidget(main_text, 0, 1, 1, -1)
1216 main_layout.addWidget(copy_button, 1, 1)
1217 main_layout.addWidget(qr_button, 1, 2)
1218 main_layout.addWidget(ok_button, 1, 3)
1219 dialog.setLayout(main_layout)
1224 def show_qrcode(title, data):
1228 d.setWindowTitle(title)
1229 d.setMinimumSize(270, 300)
1230 vbox = QVBoxLayout()
1231 qrw = QRCodeWidget(data)
1232 vbox.addWidget(qrw, 1)
1233 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1234 hbox = QHBoxLayout()
1238 filename = "qrcode.bmp"
1239 bmp.save_qrcode(qrw.qr, filename)
1240 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1242 b = QPushButton(_("Print"))
1244 b.clicked.connect(print_qr)
1246 b = QPushButton(_("Close"))
1248 b.clicked.connect(d.accept)
1250 vbox.addLayout(hbox)
1254 def sign_message(self,address):
1255 if not address: return
1258 d.setWindowTitle('Sign Message')
1259 d.setMinimumSize(410, 290)
1261 tab_widget = QTabWidget()
1263 layout = QGridLayout(tab)
1265 sign_address = QLineEdit()
1267 sign_address.setText(address)
1268 layout.addWidget(QLabel(_('Address')), 1, 0)
1269 layout.addWidget(sign_address, 1, 1)
1271 sign_message = QTextEdit()
1272 layout.addWidget(QLabel(_('Message')), 2, 0)
1273 layout.addWidget(sign_message, 2, 1)
1274 layout.setRowStretch(2,3)
1276 sign_signature = QTextEdit()
1277 layout.addWidget(QLabel(_('Signature')), 3, 0)
1278 layout.addWidget(sign_signature, 3, 1)
1279 layout.setRowStretch(3,1)
1282 if self.wallet.use_encryption:
1283 password = self.password_dialog()
1290 signature = self.wallet.sign_message(str(sign_address.text()), str(sign_message.toPlainText()), password)
1291 sign_signature.setText(signature)
1292 except BaseException, e:
1293 self.show_message(str(e))
1296 hbox = QHBoxLayout()
1297 b = QPushButton(_("Sign"))
1299 b.clicked.connect(do_sign)
1300 b = QPushButton(_("Close"))
1301 b.clicked.connect(d.accept)
1303 layout.addLayout(hbox, 4, 1)
1304 tab_widget.addTab(tab, "Sign")
1308 layout = QGridLayout(tab)
1310 verify_address = QLineEdit()
1311 layout.addWidget(QLabel(_('Address')), 1, 0)
1312 layout.addWidget(verify_address, 1, 1)
1314 verify_message = QTextEdit()
1315 layout.addWidget(QLabel(_('Message')), 2, 0)
1316 layout.addWidget(verify_message, 2, 1)
1317 layout.setRowStretch(2,3)
1319 verify_signature = QTextEdit()
1320 layout.addWidget(QLabel(_('Signature')), 3, 0)
1321 layout.addWidget(verify_signature, 3, 1)
1322 layout.setRowStretch(3,1)
1326 self.wallet.verify_message(verify_address.text(), verify_signature.text(), str(verify_message.toPlainText()))
1327 self.show_message("Signature verified")
1328 except BaseException, e:
1329 self.show_message(str(e))
1332 hbox = QHBoxLayout()
1333 b = QPushButton(_("Verify"))
1334 b.clicked.connect(do_verify)
1336 b = QPushButton(_("Close"))
1337 b.clicked.connect(d.accept)
1339 layout.addLayout(hbox, 4, 1)
1340 tab_widget.addTab(tab, "Verify")
1342 vbox = QVBoxLayout()
1343 vbox.addWidget(tab_widget)
1348 def toggle_QR_window(self, show):
1349 if show and not self.qr_window:
1350 self.qr_window = QR_Window()
1351 self.qr_window.setVisible(True)
1352 self.qr_window_geometry = self.qr_window.geometry()
1353 item = self.receive_list.currentItem()
1355 address = str(item.text(1))
1356 label = self.wallet.labels.get(address)
1357 amount = self.wallet.requested_amounts.get(address)
1358 self.qr_window.set_content( address, label, amount )
1360 elif show and self.qr_window and not self.qr_window.isVisible():
1361 self.qr_window.setVisible(True)
1362 self.qr_window.setGeometry(self.qr_window_geometry)
1364 elif not show and self.qr_window and self.qr_window.isVisible():
1365 self.qr_window_geometry = self.qr_window.geometry()
1366 self.qr_window.setVisible(False)
1368 #self.print_button.setHidden(self.qr_window is None or not self.qr_window.isVisible())
1369 self.receive_list.setColumnHidden(3, self.qr_window is None or not self.qr_window.isVisible())
1370 self.receive_list.setColumnWidth(2, 200)
1373 def question(self, msg):
1374 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1376 def show_message(self, msg):
1377 QMessageBox.information(self, _('Message'), msg, _('OK'))
1379 def password_dialog(self ):
1386 vbox = QVBoxLayout()
1387 msg = _('Please enter your password')
1388 vbox.addWidget(QLabel(msg))
1390 grid = QGridLayout()
1392 grid.addWidget(QLabel(_('Password')), 1, 0)
1393 grid.addWidget(pw, 1, 1)
1394 vbox.addLayout(grid)
1396 vbox.addLayout(ok_cancel_buttons(d))
1399 if not d.exec_(): return
1400 return unicode(pw.text())
1407 def change_password_dialog( wallet, parent=None ):
1410 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1418 new_pw = QLineEdit()
1419 new_pw.setEchoMode(2)
1420 conf_pw = QLineEdit()
1421 conf_pw.setEchoMode(2)
1423 vbox = QVBoxLayout()
1425 msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1426 +_('To disable wallet encryption, enter an empty new password.')) \
1427 if wallet.use_encryption else _('Your wallet keys are not encrypted')
1429 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1430 +_("Leave these fields empty if you want to disable encryption.")
1431 vbox.addWidget(QLabel(msg))
1433 grid = QGridLayout()
1436 if wallet.use_encryption:
1437 grid.addWidget(QLabel(_('Password')), 1, 0)
1438 grid.addWidget(pw, 1, 1)
1440 grid.addWidget(QLabel(_('New Password')), 2, 0)
1441 grid.addWidget(new_pw, 2, 1)
1443 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1444 grid.addWidget(conf_pw, 3, 1)
1445 vbox.addLayout(grid)
1447 vbox.addLayout(ok_cancel_buttons(d))
1450 if not d.exec_(): return
1452 password = unicode(pw.text()) if wallet.use_encryption else None
1453 new_password = unicode(new_pw.text())
1454 new_password2 = unicode(conf_pw.text())
1457 seed = wallet.pw_decode( wallet.seed, password)
1459 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1462 if new_password != new_password2:
1463 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1464 return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1466 wallet.update_password(seed, password, new_password)
1469 def seed_dialog(wallet, parent=None):
1473 vbox = QVBoxLayout()
1474 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1475 vbox.addWidget(QLabel(msg))
1477 grid = QGridLayout()
1480 seed_e = QLineEdit()
1481 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1482 grid.addWidget(seed_e, 1, 1)
1486 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1487 grid.addWidget(gap_e, 2, 1)
1488 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1489 vbox.addLayout(grid)
1491 vbox.addLayout(ok_cancel_buttons(d))
1494 if not d.exec_(): return
1497 gap = int(unicode(gap_e.text()))
1499 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1503 seed = unicode(seed_e.text())
1506 print_error("Warning: Not hex, trying decode")
1508 seed = mnemonic.mn_decode( seed.split(' ') )
1510 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1513 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1516 wallet.seed = str(seed)
1517 #print repr(wallet.seed)
1518 wallet.gap_limit = gap
1522 def do_import_labels(self):
1523 labelsFile = QFileDialog.getOpenFileName(QWidget(), "Open text file", util.user_dir(), self.tr("Text Files (labels.dat)"))
1524 if not labelsFile: return
1526 f = open(labelsFile, 'r')
1529 self.wallet.labels = json.loads(data)
1531 QMessageBox.information(None, "Labels imported", "Your labels where imported from '%s'" % str(labelsFile))
1532 except (IOError, os.error), reason:
1533 QMessageBox.critical(None, "Unable to export labels", "Electrum was unable to export your labels.\n" + str(reason))
1536 def do_export_labels(self):
1537 labels = self.wallet.labels
1539 labelsFile = util.user_dir() + '/labels.dat'
1540 f = open(labelsFile, 'w+')
1541 json.dump(labels, f)
1543 QMessageBox.information(None, "Labels exported", "Your labels where exported to '%s'" % str(labelsFile))
1544 except (IOError, os.error), reason:
1545 QMessageBox.critical(None, "Unable to export labels", "Electrum was unable to export your labels.\n" + str(reason))
1547 def do_export_history(self):
1548 from gui_lite import csv_transaction
1549 csv_transaction(self.wallet)
1551 def do_import_privkey(self):
1552 text, ok = QInputDialog.getText(self, _('Import private key'), _('Key') + ':')
1555 if self.wallet.use_encryption:
1556 password = self.password_dialog()
1562 addr = self.wallet.import_key(sec, password)
1564 QMessageBox.critical(None, "Unable to import key", "error")
1566 QMessageBox.information(None, "Key imported", addr)
1567 self.update_receive_tab()
1568 self.update_history_tab()
1569 except BaseException as e:
1570 QMessageBox.critical(None, "Unable to import key", str(e))
1572 def settings_dialog(self):
1574 d.setWindowTitle(_('Electrum Settings'))
1576 vbox = QVBoxLayout()
1578 tabs = QTabWidget(self)
1579 vbox.addWidget(tabs)
1582 grid_ui = QGridLayout(tab1)
1583 grid_ui.setColumnStretch(0,1)
1584 tabs.addTab(tab1, _('Display') )
1586 nz_label = QLabel(_('Display zeros'))
1587 grid_ui.addWidget(nz_label, 3, 0)
1589 nz_e.setText("%d"% self.wallet.num_zeros)
1590 grid_ui.addWidget(nz_e, 3, 1)
1591 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1592 grid_ui.addWidget(HelpButton(msg), 3, 2)
1593 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1594 if not self.config.is_modifiable('num_zeros'):
1595 for w in [nz_e, nz_label]: w.setEnabled(False)
1597 gui_label=QLabel(_('Default GUI') + ':')
1598 grid_ui.addWidget(gui_label , 7, 0)
1599 gui_combo = QComboBox()
1600 gui_combo.addItems(['Lite', 'Classic'])
1601 index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1602 if index==-1: index = 1
1603 gui_combo.setCurrentIndex(index)
1604 grid_ui.addWidget(gui_combo, 7, 1)
1605 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)
1606 if not self.config.is_modifiable('gui'):
1607 for w in [gui_combo, gui_label]: w.setEnabled(False)
1609 lang_label=QLabel(_('Language') + ':')
1610 grid_ui.addWidget(lang_label , 8, 0)
1611 lang_combo = QComboBox()
1612 from i18n import languages
1613 lang_combo.addItems(languages.values())
1615 index = languages.keys().index(self.config.get("language",''))
1618 lang_combo.setCurrentIndex(index)
1619 grid_ui.addWidget(lang_combo, 8, 1)
1620 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart). ')), 8, 2)
1621 if not self.config.is_modifiable('language'):
1622 for w in [lang_combo, lang_label]: w.setEnabled(False)
1624 currencies = self.exchanger.get_currencies()
1625 currencies.insert(0, "None")
1627 cur_label=QLabel(_('Currency') + ':')
1628 grid_ui.addWidget(cur_label , 9, 0)
1629 cur_combo = QComboBox()
1630 cur_combo.addItems(currencies)
1632 index = currencies.index(self.config.get('currency', "None"))
1635 cur_combo.setCurrentIndex(index)
1636 grid_ui.addWidget(cur_combo, 9, 1)
1637 grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes. ')), 9, 2)
1639 view_label=QLabel(_('Receive Tab') + ':')
1640 grid_ui.addWidget(view_label , 10, 0)
1641 view_combo = QComboBox()
1642 view_combo.addItems([_('Simple'), _('Advanced'), _('Point of Sale')])
1643 view_combo.setCurrentIndex(self.receive_tab_mode)
1644 grid_ui.addWidget(view_combo, 10, 1)
1645 hh = _('This selects the interaction mode of the "Receive" tab. ') + '\n\n' \
1646 + _('Simple') + ': ' + _('Show only addresses and labels.') + '\n\n' \
1647 + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' \
1648 + _('Point of Sale') + ': ' + _('Show QR code window and amounts requested for each address. Add menu item to request amount.') + '\n\n'
1650 grid_ui.addWidget(HelpButton(hh), 10, 2)
1654 grid_wallet = QGridLayout(tab2)
1655 grid_wallet.setColumnStretch(0,1)
1656 tabs.addTab(tab2, _('Wallet') )
1658 fee_label = QLabel(_('Transaction fee'))
1659 grid_wallet.addWidget(fee_label, 0, 0)
1661 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1662 grid_wallet.addWidget(fee_e, 0, 1)
1663 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1664 + _('Recommended value') + ': 0.001'
1665 grid_wallet.addWidget(HelpButton(msg), 0, 2)
1666 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1667 if not self.config.is_modifiable('fee'):
1668 for w in [fee_e, fee_label]: w.setEnabled(False)
1670 usechange_label = QLabel(_('Use change addresses'))
1671 grid_wallet.addWidget(usechange_label, 1, 0)
1672 usechange_combo = QComboBox()
1673 usechange_combo.addItems(['Yes', 'No'])
1674 usechange_combo.setCurrentIndex(not self.wallet.use_change)
1675 grid_wallet.addWidget(usechange_combo, 1, 1)
1676 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 1, 2)
1677 if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1679 gap_label = QLabel(_('Gap limit'))
1680 grid_wallet.addWidget(gap_label, 2, 0)
1682 gap_e.setText("%d"% self.wallet.gap_limit)
1683 grid_wallet.addWidget(gap_e, 2, 1)
1684 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1685 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1686 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1687 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1688 + _('Warning') + ': ' \
1689 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1690 + _('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'
1691 grid_wallet.addWidget(HelpButton(msg), 2, 2)
1692 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1693 if not self.config.is_modifiable('gap_limit'):
1694 for w in [gap_e, gap_label]: w.setEnabled(False)
1696 grid_wallet.setRowStretch(3,1)
1701 grid_io = QGridLayout(tab3)
1702 grid_io.setColumnStretch(0,1)
1703 tabs.addTab(tab3, _('Import/Export') )
1705 grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1706 grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1707 grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1708 grid_io.addWidget(HelpButton('Export your labels as json'), 1, 3)
1710 grid_io.addWidget(QLabel(_('History')), 2, 0)
1711 grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1712 grid_io.addWidget(HelpButton('Export your transaction history as csv'), 2, 3)
1714 grid_io.addWidget(QLabel(_('Private key')), 3, 0)
1715 grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
1716 grid_io.addWidget(HelpButton('Import private key' + '\n' \
1717 + _('Warning: Imported keys are not recoverable with your seed.') + '\n' \
1718 + _('If you import keys, you will need to do backups of your wallet.')), 3, 3)
1720 grid_io.setRowStretch(4,1)
1721 vbox.addLayout(ok_cancel_buttons(d))
1725 if not d.exec_(): return
1727 fee = unicode(fee_e.text())
1729 fee = int( 100000000 * Decimal(fee) )
1731 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1734 if self.wallet.fee != fee:
1735 self.wallet.fee = fee
1738 nz = unicode(nz_e.text())
1743 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1746 if self.wallet.num_zeros != nz:
1747 self.wallet.num_zeros = nz
1748 self.config.set_key('num_zeros', nz, True)
1749 self.update_history_tab()
1750 self.update_receive_tab()
1752 usechange_result = usechange_combo.currentIndex() == 0
1753 if self.wallet.use_change != usechange_result:
1754 self.wallet.use_change = usechange_result
1755 self.config.set_key('use_change', self.wallet.use_change, True)
1758 n = int(gap_e.text())
1760 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1763 if self.wallet.gap_limit != n:
1764 r = self.wallet.change_gap_limit(n)
1766 self.update_receive_tab()
1767 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1769 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1771 need_restart = False
1773 gui_request = str(gui_combo.currentText()).lower()
1774 if gui_request != self.config.get('gui'):
1775 self.config.set_key('gui', gui_request, True)
1778 lang_request = languages.keys()[lang_combo.currentIndex()]
1779 if lang_request != self.config.get('language'):
1780 self.config.set_key("language", lang_request, True)
1783 cur_request = str(currencies[cur_combo.currentIndex()])
1784 if cur_request != self.config.get('currency', "None"):
1785 self.config.set_key('currency', cur_request, True)
1786 self.update_wallet()
1789 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1791 self.receive_tab_set_mode(view_combo.currentIndex())
1795 def network_dialog(wallet, parent=None):
1796 interface = wallet.interface
1798 if interface.is_connected:
1799 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1801 status = _("Not connected")
1802 server = interface.server
1805 status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
1806 server = interface.server
1808 plist, servers_list = interface.get_servers_list()
1812 d.setWindowTitle(_('Server'))
1813 d.setMinimumSize(375, 20)
1815 vbox = QVBoxLayout()
1818 hbox = QHBoxLayout()
1820 l.setPixmap(QPixmap(":icons/network.png"))
1823 hbox.addWidget(QLabel(status))
1825 vbox.addLayout(hbox)
1829 grid = QGridLayout()
1831 vbox.addLayout(grid)
1834 server_protocol = QComboBox()
1835 server_host = QLineEdit()
1836 server_host.setFixedWidth(200)
1837 server_port = QLineEdit()
1838 server_port.setFixedWidth(60)
1840 protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1841 protocol_letters = 'thsg'
1842 DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1843 server_protocol.addItems(protocol_names)
1845 grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1846 grid.addWidget(server_protocol, 0, 1)
1847 grid.addWidget(server_host, 0, 2)
1848 grid.addWidget(server_port, 0, 3)
1850 def change_protocol(p):
1851 protocol = protocol_letters[p]
1852 host = unicode(server_host.text())
1853 pp = plist.get(host,DEFAULT_PORTS)
1854 if protocol not in pp.keys():
1855 protocol = pp.keys()[0]
1857 server_host.setText( host )
1858 server_port.setText( port )
1860 server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1862 label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1863 servers_list_widget = QTreeWidget(parent)
1864 servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
1865 servers_list_widget.setMaximumHeight(150)
1866 servers_list_widget.setColumnWidth(0, 240)
1867 for _host in servers_list.keys():
1868 _type = 'P' if servers_list[_host].get('pruning') else 'F'
1869 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
1871 def change_server(host, protocol=None):
1872 pp = plist.get(host,DEFAULT_PORTS)
1874 port = pp.get(protocol)
1875 if not port: protocol = None
1878 if 't' in pp.keys():
1880 port = pp.get(protocol)
1882 protocol = pp.keys()[0]
1883 port = pp.get(protocol)
1885 server_host.setText( host )
1886 server_port.setText( port )
1887 server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1889 if not plist: return
1890 for p in protocol_letters:
1891 i = protocol_letters.index(p)
1892 j = server_protocol.model().index(i,0)
1893 if p not in pp.keys():
1894 server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1896 server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1900 host, port, protocol = server.split(':')
1901 change_server(host,protocol)
1903 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1904 grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1906 if not wallet.config.is_modifiable('server'):
1907 for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1910 autocycle_cb = QCheckBox('Try random servers if disconnected')
1911 autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
1912 grid.addWidget(autocycle_cb, 3, 1, 3, 2)
1913 if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
1916 proxy_mode = QComboBox()
1917 proxy_host = QLineEdit()
1918 proxy_host.setFixedWidth(200)
1919 proxy_port = QLineEdit()
1920 proxy_port.setFixedWidth(60)
1921 proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1923 def check_for_disable(index = False):
1924 if proxy_mode.currentText() != 'NONE':
1925 proxy_host.setEnabled(True)
1926 proxy_port.setEnabled(True)
1928 proxy_host.setEnabled(False)
1929 proxy_port.setEnabled(False)
1932 proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1934 if not wallet.config.is_modifiable('proxy'):
1935 for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1937 proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1938 proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1939 proxy_host.setText(proxy_config.get("host"))
1940 proxy_port.setText(proxy_config.get("port"))
1942 grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1943 grid.addWidget(proxy_mode, 2, 1)
1944 grid.addWidget(proxy_host, 2, 2)
1945 grid.addWidget(proxy_port, 2, 3)
1948 vbox.addLayout(ok_cancel_buttons(d))
1951 if not d.exec_(): return
1953 server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1954 if proxy_mode.currentText() != 'NONE':
1955 proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1959 wallet.config.set_key("proxy", proxy, True)
1960 wallet.config.set_key("server", server, True)
1961 interface.set_server(server, proxy)
1962 wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
1965 def closeEvent(self, event):
1967 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1973 def __init__(self, wallet, config, app=None):
1974 self.wallet = wallet
1975 self.config = config
1977 self.app = QApplication(sys.argv)
1980 def restore_or_create(self):
1981 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1982 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1983 if r==2: return None
1984 return 'restore' if r==1 else 'create'
1986 def seed_dialog(self):
1987 return ElectrumWindow.seed_dialog( self.wallet )
1989 def network_dialog(self):
1990 return ElectrumWindow.network_dialog( self.wallet, parent=None )
1993 def show_seed(self):
1994 ElectrumWindow.show_seed_dialog(self.wallet)
1997 def password_dialog(self):
1998 ElectrumWindow.change_password_dialog(self.wallet)
2001 def restore_wallet(self):
2002 wallet = self.wallet
2003 # wait until we are connected, because the user might have selected another server
2004 if not wallet.interface.is_connected:
2005 waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
2006 waiting_dialog(waiting)
2008 waiting = lambda: False if wallet.is_up_to_date() else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
2009 %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
2011 wallet.set_up_to_date(False)
2012 wallet.interface.poke('synchronizer')
2013 waiting_dialog(waiting)
2014 if wallet.is_found():
2015 print_error( "Recovery successful" )
2017 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2024 w = ElectrumWindow(self.wallet, self.config)
2025 if url: w.set_url(url)