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 create_receive_menu(self, position):
927 # fixme: this function apparently has a side effect.
928 # if it is not called the menu pops up several times
929 #self.receive_list.selectedIndexes()
931 item = self.receive_list.itemAt(position)
933 addr = unicode(item.text(1))
935 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
936 if self.receive_tab_mode == 2:
937 menu.addAction(_("Request amount"), lambda: self.edit_amount())
938 menu.addAction(_("View QR"), lambda: ElectrumWindow.show_qrcode("Address","bitcoin:"+addr) )
939 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
940 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
942 if self.receive_tab_mode == 1:
943 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
944 menu.addAction(t, lambda: self.toggle_freeze(addr))
945 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
946 menu.addAction(t, lambda: self.toggle_priority(addr))
948 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
951 def payto(self, x, is_alias):
958 label = self.wallet.labels.get(addr)
959 m_addr = label + ' <' + addr + '>' if label else addr
960 self.tabs.setCurrentIndex(1)
961 self.payto_e.setText(m_addr)
962 self.amount_e.setFocus()
964 def delete_contact(self, x, is_alias):
965 if self.question("Do you want to remove %s from your list of contacts?"%x):
966 if not is_alias and x in self.wallet.addressbook:
967 self.wallet.addressbook.remove(x)
968 if x in self.wallet.labels.keys():
969 self.wallet.labels.pop(x)
970 elif is_alias and x in self.wallet.aliases:
971 self.wallet.aliases.pop(x)
972 self.update_history_tab()
973 self.update_contacts_tab()
974 self.update_completions()
976 def create_contact_menu(self, position):
977 # fixme: this function apparently has a side effect.
978 # if it is not called the menu pops up several times
979 #self.contacts_list.selectedIndexes()
981 item = self.contacts_list.itemAt(position)
983 addr = unicode(item.text(0))
984 label = unicode(item.text(1))
985 is_alias = label in self.wallet.aliases.keys()
986 x = label if is_alias else addr
988 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
989 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
990 menu.addAction(_("View QR code"),lambda: self.show_qrcode("Address","bitcoin:"+addr))
992 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
994 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
995 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
996 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
999 def update_receive_item(self, item):
1000 address = str( item.data(1,0).toString() )
1002 flags = self.wallet.get_address_flags(address)
1003 item.setData(0,0,flags)
1005 label = self.wallet.labels.get(address,'')
1006 item.setData(2,0,label)
1008 amount = self.wallet.requested_amounts.get(address,None)
1009 amount_str = format_satoshis( amount, False, self.wallet.num_zeros ) if amount is not None else ""
1010 item.setData(3,0,amount_str)
1012 c, u = self.wallet.get_addr_balance(address)
1013 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1014 item.setData(4,0,balance)
1016 if self.receive_tab_mode == 1:
1017 if address in self.wallet.frozen_addresses:
1018 item.setBackgroundColor(1, QColor('lightblue'))
1019 elif address in self.wallet.prioritized_addresses:
1020 item.setBackgroundColor(1, QColor('lightgreen'))
1023 def update_receive_tab(self):
1024 l = self.receive_list
1027 l.setColumnHidden(0, not self.receive_tab_mode == 1)
1028 l.setColumnHidden(3, not self.receive_tab_mode == 2)
1029 l.setColumnHidden(4, self.receive_tab_mode == 0)
1030 l.setColumnHidden(5, not self.receive_tab_mode == 1)
1031 l.setColumnWidth(0, 50)
1032 l.setColumnWidth(1, 310)
1033 l.setColumnWidth(2, 200)
1034 l.setColumnWidth(3, 130)
1035 l.setColumnWidth(4, 130)
1036 l.setColumnWidth(5, 10)
1040 for address in self.wallet.all_addresses():
1042 if self.wallet.is_change(address) and self.receive_tab_mode != 1:
1046 h = self.wallet.history.get(address,[])
1049 for tx_hash, tx_height in h:
1050 tx = self.wallet.transactions.get(tx_hash)
1058 if address in self.wallet.addresses:
1060 if gap > self.wallet.gap_limit:
1063 if address in self.wallet.addresses:
1066 item = QTreeWidgetItem( [ '', address, '', '', '', num_tx] )
1067 item.setFont(0, QFont(MONOSPACE_FONT))
1068 item.setFont(1, QFont(MONOSPACE_FONT))
1069 item.setFont(3, QFont(MONOSPACE_FONT))
1070 self.update_receive_item(item)
1071 if is_red and address in self.wallet.addresses:
1072 item.setBackgroundColor(1, QColor('red'))
1073 l.addTopLevelItem(item)
1075 # we use column 1 because column 0 may be hidden
1076 l.setCurrentItem(l.topLevelItem(0),1)
1078 def show_contact_details(self, m):
1079 a = self.wallet.aliases.get(m)
1081 if a[0] in self.wallet.authorities.keys():
1082 s = self.wallet.authorities.get(a[0])
1085 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
1086 QMessageBox.information(self, 'Alias', msg, 'OK')
1088 def update_contacts_tab(self):
1090 l = self.contacts_list
1092 l.setColumnWidth(0, 350)
1093 l.setColumnWidth(1, 330)
1094 l.setColumnWidth(2, 100)
1097 for alias, v in self.wallet.aliases.items():
1099 alias_targets.append(target)
1100 item = QTreeWidgetItem( [ target, alias, '-'] )
1101 item.setBackgroundColor(0, QColor('lightgray'))
1102 l.addTopLevelItem(item)
1104 for address in self.wallet.addressbook:
1105 if address in alias_targets: continue
1106 label = self.wallet.labels.get(address,'')
1108 for item in self.wallet.transactions.values():
1109 if address in item['outputs'] : n=n+1
1111 item = QTreeWidgetItem( [ address, label, tx] )
1112 item.setFont(0, QFont(MONOSPACE_FONT))
1113 l.addTopLevelItem(item)
1115 l.setCurrentItem(l.topLevelItem(0))
1117 def create_wall_tab(self):
1118 self.textbox = textbox = QTextEdit(self)
1119 textbox.setFont(QFont(MONOSPACE_FONT))
1120 textbox.setReadOnly(True)
1123 def create_status_bar(self):
1125 sb.setFixedHeight(35)
1126 if self.wallet.seed:
1127 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
1128 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
1129 if self.wallet.seed:
1130 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
1131 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
1132 sb.addPermanentWidget( self.status_button )
1133 self.setStatusBar(sb)
1135 def new_contact_dialog(self):
1136 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1137 address = unicode(text)
1139 if self.wallet.is_valid(address):
1140 self.wallet.addressbook.append(address)
1142 self.update_contacts_tab()
1143 self.update_history_tab()
1144 self.update_completions()
1146 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1149 def show_seed_dialog(wallet, parent=None):
1151 QMessageBox.information(parent, _('Message'),
1152 _('No seed'), _('OK'))
1155 if wallet.use_encryption:
1156 password = parent.password_dialog()
1163 seed = wallet.pw_decode(wallet.seed, password)
1165 QMessageBox.warning(parent, _('Error'),
1166 _('Incorrect Password'), _('OK'))
1169 dialog = QDialog(None)
1171 dialog.setWindowTitle("Electrum")
1173 brainwallet = ' '.join(mnemonic.mn_encode(seed))
1175 msg = _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
1176 + _("Please write down or memorize these 12 words (order is important).") + " " \
1177 + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
1178 + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
1180 main_text = QLabel(msg)
1181 main_text.setWordWrap(True)
1184 logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1191 copy_function = lambda: app.clipboard().setText(brainwallet)
1192 copy_button = QPushButton(_("Copy to Clipboard"))
1193 copy_button.clicked.connect(copy_function)
1195 show_qr_function = lambda: ElectrumWindow.show_qrcode(_("Seed"), seed)
1196 qr_button = QPushButton(_("View as QR Code"))
1197 qr_button.clicked.connect(show_qr_function)
1199 ok_button = QPushButton(_("OK"))
1200 ok_button.setDefault(True)
1201 ok_button.clicked.connect(dialog.accept)
1203 main_layout = QGridLayout()
1204 main_layout.addWidget(logo, 0, 0)
1205 main_layout.addWidget(main_text, 0, 1, 1, -1)
1206 main_layout.addWidget(copy_button, 1, 1)
1207 main_layout.addWidget(qr_button, 1, 2)
1208 main_layout.addWidget(ok_button, 1, 3)
1209 dialog.setLayout(main_layout)
1214 def show_qrcode(title, data):
1218 d.setWindowTitle(title)
1219 d.setMinimumSize(270, 300)
1220 vbox = QVBoxLayout()
1221 qrw = QRCodeWidget(data)
1222 vbox.addWidget(qrw, 1)
1223 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1224 hbox = QHBoxLayout()
1228 filename = "qrcode.bmp"
1229 bmp.save_qrcode(qrw.qr, filename)
1230 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1232 b = QPushButton(_("Print"))
1234 b.clicked.connect(print_qr)
1236 b = QPushButton(_("Close"))
1238 b.clicked.connect(d.accept)
1240 vbox.addLayout(hbox)
1244 def sign_message(self,address):
1245 if not address: return
1248 d.setWindowTitle('Sign Message')
1249 d.setMinimumSize(270, 350)
1251 tab_widget = QTabWidget()
1253 layout = QGridLayout(tab)
1255 sign_address = QLineEdit()
1256 sign_address.setText(address)
1257 layout.addWidget(QLabel(_('Address')), 1, 0)
1258 layout.addWidget(sign_address, 1, 1)
1260 sign_message = QTextEdit()
1261 layout.addWidget(QLabel(_('Message')), 2, 0)
1262 layout.addWidget(sign_message, 2, 1, 2, 1)
1264 sign_signature = QLineEdit()
1265 layout.addWidget(QLabel(_('Signature')), 3, 0)
1266 layout.addWidget(sign_signature, 3, 1)
1269 if self.wallet.use_encryption:
1270 password = self.password_dialog()
1277 signature = self.wallet.sign_message(sign_address.text(), str(sign_message.toPlainText()), password)
1278 sign_signature.setText(signature)
1279 except BaseException, e:
1280 self.show_message(str(e))
1283 hbox = QHBoxLayout()
1284 b = QPushButton(_("Sign"))
1286 b.clicked.connect(do_sign)
1287 b = QPushButton(_("Close"))
1288 b.clicked.connect(d.accept)
1290 layout.addLayout(hbox, 4, 1)
1291 tab_widget.addTab(tab, "Sign")
1295 layout = QGridLayout(tab)
1297 verify_address = QLineEdit()
1298 layout.addWidget(QLabel(_('Address')), 1, 0)
1299 layout.addWidget(verify_address, 1, 1)
1301 verify_message = QTextEdit()
1302 layout.addWidget(QLabel(_('Message')), 2, 0)
1303 layout.addWidget(verify_message, 2, 1, 2, 1)
1305 verify_signature = QLineEdit()
1306 layout.addWidget(QLabel(_('Signature')), 3, 0)
1307 layout.addWidget(verify_signature, 3, 1)
1311 self.wallet.verify_message(verify_address.text(), verify_signature.text(), str(verify_message.toPlainText()))
1312 self.show_message("Signature verified")
1313 except BaseException, e:
1314 self.show_message(str(e))
1317 hbox = QHBoxLayout()
1318 b = QPushButton(_("Verify"))
1319 b.clicked.connect(do_verify)
1321 b = QPushButton(_("Close"))
1322 b.clicked.connect(d.accept)
1324 layout.addLayout(hbox, 4, 1)
1325 tab_widget.addTab(tab, "Verify")
1327 vbox = QVBoxLayout()
1328 vbox.addWidget(tab_widget)
1333 def toggle_QR_window(self, show):
1334 if show and not self.qr_window:
1335 self.qr_window = QR_Window()
1336 self.qr_window.setVisible(True)
1337 self.qr_window_geometry = self.qr_window.geometry()
1338 item = self.receive_list.currentItem()
1340 address = str(item.text(1))
1341 label = self.wallet.labels.get(address)
1342 amount = self.wallet.requested_amounts.get(address)
1343 self.qr_window.set_content( address, label, amount )
1345 elif show and self.qr_window and not self.qr_window.isVisible():
1346 self.qr_window.setVisible(True)
1347 self.qr_window.setGeometry(self.qr_window_geometry)
1349 elif not show and self.qr_window and self.qr_window.isVisible():
1350 self.qr_window_geometry = self.qr_window.geometry()
1351 self.qr_window.setVisible(False)
1353 #self.print_button.setHidden(self.qr_window is None or not self.qr_window.isVisible())
1354 self.receive_list.setColumnHidden(3, self.qr_window is None or not self.qr_window.isVisible())
1355 self.receive_list.setColumnWidth(2, 200)
1358 def question(self, msg):
1359 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1361 def show_message(self, msg):
1362 QMessageBox.information(self, _('Message'), msg, _('OK'))
1364 def password_dialog(self ):
1371 vbox = QVBoxLayout()
1372 msg = _('Please enter your password')
1373 vbox.addWidget(QLabel(msg))
1375 grid = QGridLayout()
1377 grid.addWidget(QLabel(_('Password')), 1, 0)
1378 grid.addWidget(pw, 1, 1)
1379 vbox.addLayout(grid)
1381 vbox.addLayout(ok_cancel_buttons(d))
1384 if not d.exec_(): return
1385 return unicode(pw.text())
1392 def change_password_dialog( wallet, parent=None ):
1395 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1403 new_pw = QLineEdit()
1404 new_pw.setEchoMode(2)
1405 conf_pw = QLineEdit()
1406 conf_pw.setEchoMode(2)
1408 vbox = QVBoxLayout()
1410 msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1411 +_('To disable wallet encryption, enter an empty new password.')) \
1412 if wallet.use_encryption else _('Your wallet keys are not encrypted')
1414 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1415 +_("Leave these fields empty if you want to disable encryption.")
1416 vbox.addWidget(QLabel(msg))
1418 grid = QGridLayout()
1421 if wallet.use_encryption:
1422 grid.addWidget(QLabel(_('Password')), 1, 0)
1423 grid.addWidget(pw, 1, 1)
1425 grid.addWidget(QLabel(_('New Password')), 2, 0)
1426 grid.addWidget(new_pw, 2, 1)
1428 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1429 grid.addWidget(conf_pw, 3, 1)
1430 vbox.addLayout(grid)
1432 vbox.addLayout(ok_cancel_buttons(d))
1435 if not d.exec_(): return
1437 password = unicode(pw.text()) if wallet.use_encryption else None
1438 new_password = unicode(new_pw.text())
1439 new_password2 = unicode(conf_pw.text())
1442 seed = wallet.pw_decode( wallet.seed, password)
1444 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1447 if new_password != new_password2:
1448 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1449 return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1451 wallet.update_password(seed, password, new_password)
1454 def seed_dialog(wallet, parent=None):
1458 vbox = QVBoxLayout()
1459 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1460 vbox.addWidget(QLabel(msg))
1462 grid = QGridLayout()
1465 seed_e = QLineEdit()
1466 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1467 grid.addWidget(seed_e, 1, 1)
1471 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1472 grid.addWidget(gap_e, 2, 1)
1473 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1474 vbox.addLayout(grid)
1476 vbox.addLayout(ok_cancel_buttons(d))
1479 if not d.exec_(): return
1482 gap = int(unicode(gap_e.text()))
1484 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1488 seed = unicode(seed_e.text())
1491 print_error("Warning: Not hex, trying decode")
1493 seed = mnemonic.mn_decode( seed.split(' ') )
1495 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1498 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1501 wallet.seed = str(seed)
1502 #print repr(wallet.seed)
1503 wallet.gap_limit = gap
1507 def do_import_labels(self):
1508 labelsFile = QFileDialog.getOpenFileName(QWidget(), "Open text file", util.user_dir(), self.tr("Text Files (labels.dat)"))
1509 if not labelsFile: return
1511 f = open(labelsFile, 'r')
1514 self.wallet.labels = json.loads(data)
1516 QMessageBox.information(None, "Labels imported", "Your labels where imported from '%s'" % str(labelsFile))
1517 except (IOError, os.error), reason:
1518 QMessageBox.critical(None, "Unable to export labels", "Electrum was unable to export your labels.\n" + str(reason))
1521 def do_export_labels(self):
1522 labels = self.wallet.labels
1524 labelsFile = util.user_dir() + '/labels.dat'
1525 f = open(labelsFile, 'w+')
1526 json.dump(labels, f)
1528 QMessageBox.information(None, "Labels exported", "Your labels where exported to '%s'" % str(labelsFile))
1529 except (IOError, os.error), reason:
1530 QMessageBox.critical(None, "Unable to export labels", "Electrum was unable to export your labels.\n" + str(reason))
1532 def do_export_history(self):
1533 from gui_lite import csv_transaction
1534 csv_transaction(self.wallet)
1536 def do_import_privkey(self):
1537 text, ok = QInputDialog.getText(self, _('Import private key'), _('Key') + ':')
1540 if self.wallet.use_encryption:
1541 password = self.password_dialog()
1547 addr = self.wallet.import_key(sec, password)
1549 QMessageBox.critical(None, "Unable to import key", "error")
1551 QMessageBox.information(None, "Key imported", addr)
1552 except BaseException as e:
1553 QMessageBox.critical(None, "Unable to import key", str(e))
1555 def settings_dialog(self):
1557 d.setWindowTitle(_('Electrum Settings'))
1559 vbox = QVBoxLayout()
1561 tabs = QTabWidget(self)
1562 vbox.addWidget(tabs)
1565 grid_ui = QGridLayout(tab1)
1566 grid_ui.setColumnStretch(0,1)
1567 tabs.addTab(tab1, _('Display') )
1569 nz_label = QLabel(_('Display zeros'))
1570 grid_ui.addWidget(nz_label, 3, 0)
1572 nz_e.setText("%d"% self.wallet.num_zeros)
1573 grid_ui.addWidget(nz_e, 3, 1)
1574 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1575 grid_ui.addWidget(HelpButton(msg), 3, 2)
1576 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1577 if not self.config.is_modifiable('num_zeros'):
1578 for w in [nz_e, nz_label]: w.setEnabled(False)
1580 gui_label=QLabel(_('Default GUI') + ':')
1581 grid_ui.addWidget(gui_label , 7, 0)
1582 gui_combo = QComboBox()
1583 gui_combo.addItems(['Lite', 'Classic'])
1584 index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1585 if index==-1: index = 1
1586 gui_combo.setCurrentIndex(index)
1587 grid_ui.addWidget(gui_combo, 7, 1)
1588 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)
1589 if not self.config.is_modifiable('gui'):
1590 for w in [gui_combo, gui_label]: w.setEnabled(False)
1592 lang_label=QLabel(_('Language') + ':')
1593 grid_ui.addWidget(lang_label , 8, 0)
1594 lang_combo = QComboBox()
1595 from i18n import languages
1596 lang_combo.addItems(languages.values())
1598 index = languages.keys().index(self.config.get("language",''))
1601 lang_combo.setCurrentIndex(index)
1602 grid_ui.addWidget(lang_combo, 8, 1)
1603 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart). ')), 8, 2)
1604 if not self.config.is_modifiable('language'):
1605 for w in [lang_combo, lang_label]: w.setEnabled(False)
1607 currencies = self.exchanger.get_currencies()
1608 currencies.insert(0, "None")
1610 cur_label=QLabel(_('Currency') + ':')
1611 grid_ui.addWidget(cur_label , 9, 0)
1612 cur_combo = QComboBox()
1613 cur_combo.addItems(currencies)
1615 index = currencies.index(self.config.get('currency', "None"))
1618 cur_combo.setCurrentIndex(index)
1619 grid_ui.addWidget(cur_combo, 9, 1)
1620 grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes. ')), 9, 2)
1622 view_label=QLabel(_('Receive Tab') + ':')
1623 grid_ui.addWidget(view_label , 10, 0)
1624 view_combo = QComboBox()
1625 view_combo.addItems([_('Simple'), _('Advanced'), _('Point of Sale')])
1626 view_combo.setCurrentIndex(self.receive_tab_mode)
1627 grid_ui.addWidget(view_combo, 10, 1)
1628 hh = _('This selects the interaction mode of the "Receive" tab. ') + '\n\n' \
1629 + _('Simple') + ': ' + _('Show only addresses and labels.') + '\n\n' \
1630 + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' \
1631 + _('Point of Sale') + ': ' + _('Show QR code window and amounts requested for each address. Add menu item to request amount.') + '\n\n'
1633 grid_ui.addWidget(HelpButton(hh), 10, 2)
1637 grid_wallet = QGridLayout(tab2)
1638 grid_wallet.setColumnStretch(0,1)
1639 tabs.addTab(tab2, _('Wallet') )
1641 fee_label = QLabel(_('Transaction fee'))
1642 grid_wallet.addWidget(fee_label, 0, 0)
1644 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1645 grid_wallet.addWidget(fee_e, 0, 1)
1646 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1647 + _('Recommended value') + ': 0.001'
1648 grid_wallet.addWidget(HelpButton(msg), 0, 2)
1649 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1650 if not self.config.is_modifiable('fee'):
1651 for w in [fee_e, fee_label]: w.setEnabled(False)
1653 usechange_label = QLabel(_('Use change addresses'))
1654 grid_wallet.addWidget(usechange_label, 1, 0)
1655 usechange_combo = QComboBox()
1656 usechange_combo.addItems(['Yes', 'No'])
1657 usechange_combo.setCurrentIndex(not self.wallet.use_change)
1658 grid_wallet.addWidget(usechange_combo, 1, 1)
1659 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 1, 2)
1660 if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1662 gap_label = QLabel(_('Gap limit'))
1663 grid_wallet.addWidget(gap_label, 2, 0)
1665 gap_e.setText("%d"% self.wallet.gap_limit)
1666 grid_wallet.addWidget(gap_e, 2, 1)
1667 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1668 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1669 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1670 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1671 + _('Warning') + ': ' \
1672 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1673 + _('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'
1674 grid_wallet.addWidget(HelpButton(msg), 2, 2)
1675 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1676 if not self.config.is_modifiable('gap_limit'):
1677 for w in [gap_e, gap_label]: w.setEnabled(False)
1679 grid_wallet.setRowStretch(3,1)
1684 grid_io = QGridLayout(tab3)
1685 grid_io.setColumnStretch(0,1)
1686 tabs.addTab(tab3, _('Import/Export') )
1688 grid_io.addWidget(QLabel(_('Labels')), 1, 0)
1689 grid_io.addWidget(EnterButton(_("Export"), self.do_export_labels), 1, 1)
1690 grid_io.addWidget(EnterButton(_("Import"), self.do_import_labels), 1, 2)
1691 grid_io.addWidget(HelpButton('Export your labels as json'), 1, 3)
1693 grid_io.addWidget(QLabel(_('History')), 2, 0)
1694 grid_io.addWidget(EnterButton(_("Export"), self.do_export_history), 2, 1)
1695 grid_io.addWidget(HelpButton('Export your transaction history as csv'), 2, 3)
1697 grid_io.addWidget(QLabel(_('Private key')), 3, 0)
1698 grid_io.addWidget(EnterButton(_("Import"), self.do_import_privkey), 3, 2)
1699 grid_io.addWidget(HelpButton('Import private key' + '\n' \
1700 + _('Warning: Imported keys are not recoverable with your seed.') + '\n' \
1701 + _('If you import keys, you will need to do backups of your wallet.')), 3, 3)
1703 grid_io.setRowStretch(4,1)
1704 vbox.addLayout(ok_cancel_buttons(d))
1708 if not d.exec_(): return
1710 fee = unicode(fee_e.text())
1712 fee = int( 100000000 * Decimal(fee) )
1714 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1717 if self.wallet.fee != fee:
1718 self.wallet.fee = fee
1721 nz = unicode(nz_e.text())
1726 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1729 if self.wallet.num_zeros != nz:
1730 self.wallet.num_zeros = nz
1731 self.config.set_key('num_zeros', nz, True)
1732 self.update_history_tab()
1733 self.update_receive_tab()
1735 usechange_result = usechange_combo.currentIndex() == 0
1736 if self.wallet.use_change != usechange_result:
1737 self.wallet.use_change = usechange_result
1738 self.config.set_key('use_change', self.wallet.use_change, True)
1741 n = int(gap_e.text())
1743 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1746 if self.wallet.gap_limit != n:
1747 r = self.wallet.change_gap_limit(n)
1749 self.update_receive_tab()
1750 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1752 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1754 need_restart = False
1756 gui_request = str(gui_combo.currentText()).lower()
1757 if gui_request != self.config.get('gui'):
1758 self.config.set_key('gui', gui_request, True)
1761 lang_request = languages.keys()[lang_combo.currentIndex()]
1762 if lang_request != self.config.get('language'):
1763 self.config.set_key("language", lang_request, True)
1766 cur_request = str(currencies[cur_combo.currentIndex()])
1767 if cur_request != self.config.get('currency', "None"):
1768 self.config.set_key('currency', cur_request, True)
1769 self.update_wallet()
1772 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1774 self.receive_tab_set_mode(view_combo.currentIndex())
1778 def network_dialog(wallet, parent=None):
1779 interface = wallet.interface
1781 if interface.is_connected:
1782 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1784 status = _("Not connected")
1785 server = interface.server
1788 status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
1789 server = interface.server
1791 plist, servers_list = interface.get_servers_list()
1795 d.setWindowTitle(_('Server'))
1796 d.setMinimumSize(375, 20)
1798 vbox = QVBoxLayout()
1801 hbox = QHBoxLayout()
1803 l.setPixmap(QPixmap(":icons/network.png"))
1806 hbox.addWidget(QLabel(status))
1808 vbox.addLayout(hbox)
1812 grid = QGridLayout()
1814 vbox.addLayout(grid)
1817 server_protocol = QComboBox()
1818 server_host = QLineEdit()
1819 server_host.setFixedWidth(200)
1820 server_port = QLineEdit()
1821 server_port.setFixedWidth(60)
1823 protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1824 protocol_letters = 'thsg'
1825 DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1826 server_protocol.addItems(protocol_names)
1828 grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1829 grid.addWidget(server_protocol, 0, 1)
1830 grid.addWidget(server_host, 0, 2)
1831 grid.addWidget(server_port, 0, 3)
1833 def change_protocol(p):
1834 protocol = protocol_letters[p]
1835 host = unicode(server_host.text())
1836 pp = plist.get(host,DEFAULT_PORTS)
1837 if protocol not in pp.keys():
1838 protocol = pp.keys()[0]
1840 server_host.setText( host )
1841 server_port.setText( port )
1843 server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1845 label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1846 servers_list_widget = QTreeWidget(parent)
1847 servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
1848 servers_list_widget.setMaximumHeight(150)
1849 servers_list_widget.setColumnWidth(0, 240)
1850 for _host in servers_list.keys():
1851 _type = 'P' if servers_list[_host].get('pruning') else 'F'
1852 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
1854 def change_server(host, protocol=None):
1855 pp = plist.get(host,DEFAULT_PORTS)
1857 port = pp.get(protocol)
1858 if not port: protocol = None
1861 if 't' in pp.keys():
1863 port = pp.get(protocol)
1865 protocol = pp.keys()[0]
1866 port = pp.get(protocol)
1868 server_host.setText( host )
1869 server_port.setText( port )
1870 server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1872 if not plist: return
1873 for p in protocol_letters:
1874 i = protocol_letters.index(p)
1875 j = server_protocol.model().index(i,0)
1876 if p not in pp.keys():
1877 server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1879 server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1883 host, port, protocol = server.split(':')
1884 change_server(host,protocol)
1886 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1887 grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1889 if not wallet.config.is_modifiable('server'):
1890 for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1893 autocycle_cb = QCheckBox('Try random servers if disconnected')
1894 autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
1895 grid.addWidget(autocycle_cb, 3, 1, 3, 2)
1896 if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
1899 proxy_mode = QComboBox()
1900 proxy_host = QLineEdit()
1901 proxy_host.setFixedWidth(200)
1902 proxy_port = QLineEdit()
1903 proxy_port.setFixedWidth(60)
1904 proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1906 def check_for_disable(index = False):
1907 if proxy_mode.currentText() != 'NONE':
1908 proxy_host.setEnabled(True)
1909 proxy_port.setEnabled(True)
1911 proxy_host.setEnabled(False)
1912 proxy_port.setEnabled(False)
1915 proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1917 if not wallet.config.is_modifiable('proxy'):
1918 for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1920 proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1921 proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1922 proxy_host.setText(proxy_config.get("host"))
1923 proxy_port.setText(proxy_config.get("port"))
1925 grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1926 grid.addWidget(proxy_mode, 2, 1)
1927 grid.addWidget(proxy_host, 2, 2)
1928 grid.addWidget(proxy_port, 2, 3)
1931 vbox.addLayout(ok_cancel_buttons(d))
1934 if not d.exec_(): return
1936 server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1937 if proxy_mode.currentText() != 'NONE':
1938 proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1942 wallet.config.set_key("proxy", proxy, True)
1943 wallet.config.set_key("server", server, True)
1944 interface.set_server(server, proxy)
1945 wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
1948 def closeEvent(self, event):
1950 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1956 def __init__(self, wallet, config, app=None):
1957 self.wallet = wallet
1958 self.config = config
1960 self.app = QApplication(sys.argv)
1963 def restore_or_create(self):
1964 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1965 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1966 if r==2: return None
1967 return 'restore' if r==1 else 'create'
1969 def seed_dialog(self):
1970 return ElectrumWindow.seed_dialog( self.wallet )
1972 def network_dialog(self):
1973 return ElectrumWindow.network_dialog( self.wallet, parent=None )
1976 def show_seed(self):
1977 ElectrumWindow.show_seed_dialog(self.wallet)
1980 def password_dialog(self):
1981 ElectrumWindow.change_password_dialog(self.wallet)
1984 def restore_wallet(self):
1985 wallet = self.wallet
1986 # wait until we are connected, because the user might have selected another server
1987 if not wallet.interface.is_connected:
1988 waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
1989 waiting_dialog(waiting)
1991 waiting = lambda: False if wallet.is_up_to_date() else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1992 %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
1994 wallet.set_up_to_date(False)
1995 wallet.interface.poke('synchronizer')
1996 waiting_dialog(waiting)
1997 if wallet.is_found():
1998 print_error( "Recovery successful" )
2000 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
2007 w = ElectrumWindow(self.wallet, self.config)
2008 if url: w.set_url(url)