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
26 sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
31 import PyQt4.QtGui as QtGui
32 from interface import DEFAULT_SERVERS
37 sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'")
39 from wallet import format_satoshis
40 import bmp, mnemonic, pyqrnative, qrscanner
43 from decimal import Decimal
47 if platform.system() == 'Windows':
48 MONOSPACE_FONT = 'Lucida Console'
49 elif platform.system() == 'Darwin':
50 MONOSPACE_FONT = 'Monaco'
52 MONOSPACE_FONT = 'monospace'
54 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
56 def numbify(entry, is_int = False):
57 text = unicode(entry.text()).strip()
58 pos = entry.cursorPosition()
60 if not is_int: chars +='.'
61 s = ''.join([i for i in text if i in chars])
66 s = s[:p] + '.' + s[p:p+8]
68 amount = int( Decimal(s) * 100000000 )
77 entry.setCursorPosition(pos)
81 class Timer(QtCore.QThread):
84 self.emit(QtCore.SIGNAL('timersignal'))
87 class HelpButton(QPushButton):
88 def __init__(self, text):
89 QPushButton.__init__(self, '?')
90 self.setFocusPolicy(Qt.NoFocus)
91 self.setFixedWidth(20)
92 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
95 class EnterButton(QPushButton):
96 def __init__(self, text, func):
97 QPushButton.__init__(self, text)
99 self.clicked.connect(func)
101 def keyPressEvent(self, e):
102 if e.key() == QtCore.Qt.Key_Return:
105 class MyTreeWidget(QTreeWidget):
106 def __init__(self, parent):
107 QTreeWidget.__init__(self, parent)
110 for i in range(0,self.viewport().height()/5):
111 if self.itemAt(QPoint(0,i*5)) == item:
115 for j in range(0,30):
116 if self.itemAt(QPoint(0,i*5 + j)) != item:
118 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
120 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
125 class StatusBarButton(QPushButton):
126 def __init__(self, icon, tooltip, func):
127 QPushButton.__init__(self, icon, '')
128 self.setToolTip(tooltip)
130 self.setMaximumWidth(25)
131 self.clicked.connect(func)
134 def keyPressEvent(self, e):
135 if e.key() == QtCore.Qt.Key_Return:
139 class QRCodeWidget(QWidget):
141 def __init__(self, data = None):
142 QWidget.__init__(self)
143 self.setMinimumSize(210, 210)
150 def set_addr(self, addr):
151 if self.addr != addr:
157 if self.addr and not self.qr:
158 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
159 self.qr.addData(self.addr)
163 def paintEvent(self, e):
168 black = QColor(0, 0, 0, 255)
169 white = QColor(255, 255, 255, 255)
172 qp = QtGui.QPainter()
176 qp.drawRect(0, 0, 198, 198)
180 k = self.qr.getModuleCount()
181 qp = QtGui.QPainter()
184 boxsize = min(r.width(), r.height())*0.8/k
186 left = (r.width() - size)/2
187 top = (r.height() - size)/2
191 if self.qr.isDark(r, c):
197 qp.drawRect(left+c*boxsize, top+r*boxsize, boxsize, boxsize)
202 class QR_Window(QWidget):
205 QWidget.__init__(self)
206 self.setWindowTitle('Electrum - Invoice')
207 self.setMinimumSize(800, 250)
211 self.setFocusPolicy(QtCore.Qt.NoFocus)
213 main_box = QHBoxLayout()
215 self.qrw = QRCodeWidget()
216 main_box.addWidget(self.qrw, 1)
219 main_box.addLayout(vbox)
221 self.address_label = QLabel("")
222 self.address_label.setFont(QFont(MONOSPACE_FONT))
223 vbox.addWidget(self.address_label)
225 self.label_label = QLabel("")
226 vbox.addWidget(self.label_label)
228 self.amount_label = QLabel("")
229 vbox.addWidget(self.amount_label)
232 self.setLayout(main_box)
235 def set_content(self, addr, label, amount):
237 address_text = "<span style='font-size: 18pt'>%s</span>" % addr if addr else ""
238 self.address_label.setText(address_text)
241 amount_text = "<span style='font-size: 21pt'>%s</span> <span style='font-size: 16pt'>BTC</span> " % format_satoshis(amount) if amount else ""
242 self.amount_label.setText(amount_text)
245 label_text = "<span style='font-size: 21pt'>%s</span>" % label if label else ""
246 self.label_label.setText(label_text)
248 msg = 'bitcoin:'+self.address
249 if self.amount is not None:
250 msg += '?amount=%s'%(str( Decimal(self.amount) /100000000))
251 if self.label is not None:
252 msg += '&label=%s'%(self.label)
253 elif self.label is not None:
254 msg += '?label=%s'%(self.label)
256 self.qrw.set_addr( msg )
261 def waiting_dialog(f):
267 w.setWindowTitle('Electrum')
277 w.connect(s, QtCore.SIGNAL('timersignal'), ff)
282 def ok_cancel_buttons(dialog):
285 b = QPushButton("OK")
287 b.clicked.connect(dialog.accept)
288 b = QPushButton("Cancel")
290 b.clicked.connect(dialog.reject)
294 class ElectrumWindow(QMainWindow):
296 def __init__(self, wallet, config):
297 QMainWindow.__init__(self)
300 self.wallet.interface.register_callback('updated', self.update_callback)
301 self.wallet.interface.register_callback('connected', self.update_callback)
302 self.wallet.interface.register_callback('disconnected', self.update_callback)
303 self.wallet.interface.register_callback('disconnecting', self.update_callback)
305 self.receive_tab_mode = config.get('qt_receive_tab_mode', 0)
306 self.merchant_name = config.get('merchant_name', 'Invoice')
308 self.qr_window = None
309 self.funds_error = False
310 self.completions = QStringListModel()
312 self.tabs = tabs = QTabWidget(self)
313 tabs.addTab(self.create_history_tab(), _('History') )
314 tabs.addTab(self.create_send_tab(), _('Send') )
315 tabs.addTab(self.create_receive_tab(), _('Receive') )
316 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
317 tabs.addTab(self.create_wall_tab(), _('Wall') )
318 tabs.setMinimumSize(600, 400)
319 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
320 self.setCentralWidget(tabs)
321 self.create_status_bar()
322 self.toggle_QR_window(self.receive_tab_mode == 2)
324 g = self.config.get("winpos-qt",[100, 100, 840, 400])
325 self.setGeometry(g[0], g[1], g[2], g[3])
326 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
327 if not self.wallet.seed: title += ' [seedless]'
328 self.setWindowTitle( title )
330 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
331 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
332 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
333 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
335 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
336 #self.connect(self, SIGNAL('editamount'), self.edit_amount)
337 self.history_list.setFocus(True)
339 self.exchanger = exchange_rate.Exchanger(self)
340 self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet)
342 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
343 if platform.system() == 'Windows':
344 n = 3 if self.wallet.seed else 2
345 tabs.setCurrentIndex (n)
346 tabs.setCurrentIndex (0)
349 QMainWindow.close(self)
351 self.qr_window.close()
352 self.qr_window = None
354 def connect_slots(self, sender):
355 self.connect(sender, QtCore.SIGNAL('timersignal'), self.timer_actions)
356 self.previous_payto_e=''
358 def timer_actions(self):
360 self.qr_window.qrw.update_qr()
362 if self.payto_e.hasFocus():
364 r = unicode( self.payto_e.text() )
365 if r != self.previous_payto_e:
366 self.previous_payto_e = r
368 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
370 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
374 s = r + ' <' + to_address + '>'
375 self.payto_e.setText(s)
378 def update_callback(self):
379 self.emit(QtCore.SIGNAL('updatesignal'))
381 def update_wallet(self):
382 if self.wallet.interface and self.wallet.interface.is_connected:
383 if not self.wallet.up_to_date:
384 text = _( "Synchronizing..." )
385 icon = QIcon(":icons/status_waiting.png")
387 c, u = self.wallet.get_balance()
388 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
389 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
390 text += self.create_quote_text(Decimal(c+u)/100000000)
391 icon = QIcon(":icons/status_connected.png")
393 text = _( "Not connected" )
394 icon = QIcon(":icons/status_disconnected.png")
397 text = _( "Not enough funds" )
399 self.statusBar().showMessage(text)
400 self.status_button.setIcon( icon )
402 if self.wallet.up_to_date or not self.wallet.interface.is_connected:
403 self.textbox.setText( self.wallet.banner )
404 self.update_history_tab()
405 self.update_receive_tab()
406 self.update_contacts_tab()
407 self.update_completions()
409 def create_quote_text(self, btc_balance):
410 quote_currency = self.config.get("currency", "None")
411 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
412 if quote_balance is None:
415 quote_text = " (%.2f %s)" % (quote_balance, quote_currency)
418 def create_history_tab(self):
419 self.history_list = l = MyTreeWidget(self)
421 l.setColumnWidth(0, 40)
422 l.setColumnWidth(1, 140)
423 l.setColumnWidth(2, 350)
424 l.setColumnWidth(3, 140)
425 l.setColumnWidth(4, 140)
426 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
427 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
428 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
430 l.setContextMenuPolicy(Qt.CustomContextMenu)
431 l.customContextMenuRequested.connect(self.create_history_menu)
435 def create_history_menu(self, position):
436 self.history_list.selectedIndexes()
437 item = self.history_list.currentItem()
439 tx_hash = str(item.data(0, Qt.UserRole).toString())
440 if not tx_hash: return
442 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
443 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
444 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
445 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
448 def tx_details(self, tx_hash):
449 tx_details = self.wallet.get_tx_details(tx_hash)
450 QMessageBox.information(self, 'Details', tx_details, 'OK')
453 def tx_label_clicked(self, item, column):
454 if column==2 and item.isSelected():
455 tx_hash = str(item.toolTip(0))
457 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
458 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
459 self.history_list.editItem( item, column )
460 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
463 def tx_label_changed(self, item, column):
467 tx_hash = str(item.toolTip(0))
468 tx = self.wallet.transactions.get(tx_hash)
469 s = self.wallet.labels.get(tx_hash)
470 text = unicode( item.text(2) )
472 self.wallet.labels[tx_hash] = text
473 item.setForeground(2, QBrush(QColor('black')))
475 if s: self.wallet.labels.pop(tx_hash)
476 text = self.wallet.get_default_label(tx_hash)
477 item.setText(2, text)
478 item.setForeground(2, QBrush(QColor('gray')))
482 def edit_label(self, is_recv):
483 l = self.receive_list if is_recv else self.contacts_list
484 c = 2 if is_recv else 1
485 item = l.currentItem()
486 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
487 l.editItem( item, c )
488 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
490 def edit_amount(self):
491 l = self.receive_list
492 item = l.currentItem()
493 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
494 l.editItem( item, 3 )
495 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
498 def address_label_clicked(self, item, column, l, column_addr, column_label):
499 if column == column_label and item.isSelected():
500 addr = unicode( item.text(column_addr) )
501 label = unicode( item.text(column_label) )
502 if label in self.wallet.aliases.keys():
504 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
505 l.editItem( item, column )
506 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
509 def address_label_changed(self, item, column, l, column_addr, column_label):
511 if column == column_label:
512 addr = unicode( item.text(column_addr) )
513 text = unicode( item.text(column_label) )
517 if text not in self.wallet.aliases.keys():
518 old_addr = self.wallet.labels.get(text)
520 self.wallet.labels[addr] = text
523 print_error("Error: This is one of your aliases")
524 label = self.wallet.labels.get(addr,'')
525 item.setText(column_label, QString(label))
527 s = self.wallet.labels.get(addr)
529 self.wallet.labels.pop(addr)
533 self.update_history_tab()
534 self.update_completions()
536 self.recv_changed(item)
539 address = unicode( item.text(column_addr) )
540 text = unicode( item.text(3) )
542 index = self.wallet.addresses.index(address)
547 amount = int( Decimal(text) * 100000000 )
548 item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
550 amount = self.wallet.requested_amounts.get(address)
552 item.setText(3,format_satoshis(amount,False, self.wallet.num_zeros))
557 self.wallet.requested_amounts[address] = amount
559 label = self.wallet.labels.get(address)
561 label = self.merchant_name + ' - %04d'%(index+1)
562 self.wallet.labels[address] = label
564 self.update_receive_item(self.receive_list.currentItem())
566 self.qr_window.set_content( address, label, amount )
569 def recv_changed(self, a):
570 "current item changed"
571 if a is not None and self.qr_window and self.qr_window.isVisible():
572 address = str(a.text(1))
573 label = self.wallet.labels.get(address)
574 amount = self.wallet.requested_amounts.get(address)
575 self.qr_window.set_content( address, label, amount )
578 def update_history_tab(self):
580 self.history_list.clear()
581 for item in self.wallet.get_tx_history():
582 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
585 time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
591 icon = QIcon(":icons/unconfirmed.png")
593 icon = QIcon(":icons/clock%d.png"%conf)
595 icon = QIcon(":icons/confirmed.png")
598 icon = QIcon(":icons/unconfirmed.png")
600 if value is not None:
601 v_str = format_satoshis(value, True, self.wallet.num_zeros)
605 balance_str = format_satoshis(balance, False, self.wallet.num_zeros)
608 label, is_default_label = self.wallet.get_label(tx_hash)
610 label = _('Pruned transaction outputs')
611 is_default_label = False
613 item = QTreeWidgetItem( [ '', time_str, label, v_str, balance_str] )
614 item.setFont(2, QFont(MONOSPACE_FONT))
615 item.setFont(3, QFont(MONOSPACE_FONT))
616 item.setFont(4, QFont(MONOSPACE_FONT))
618 item.setForeground(3, QBrush(QColor("#BC1E1E")))
620 item.setData(0, Qt.UserRole, tx_hash)
621 item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
623 item.setForeground(2, QBrush(QColor('grey')))
625 item.setIcon(0, icon)
626 self.history_list.insertTopLevelItem(0,item)
629 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
632 def create_send_tab(self):
637 grid.setColumnMinimumWidth(3,300)
638 grid.setColumnStretch(5,1)
640 self.payto_e = QLineEdit()
641 grid.addWidget(QLabel(_('Pay to')), 1, 0)
642 grid.addWidget(self.payto_e, 1, 1, 1, 3)
645 qrcode = qrscanner.scan_qr()
646 if 'address' in qrcode:
647 self.payto_e.setText(qrcode['address'])
648 if 'amount' in qrcode:
649 self.amount_e.setText(str(qrcode['amount']))
650 if 'label' in qrcode:
651 self.message_e.setText(qrcode['label'])
652 if 'message' in qrcode:
653 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
656 if qrscanner.is_available():
657 b = QPushButton(_("Scan QR code"))
658 b.clicked.connect(fill_from_qr)
659 grid.addWidget(b, 1, 5)
661 grid.addWidget(HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)')), 1, 4)
663 completer = QCompleter()
664 completer.setCaseSensitivity(False)
665 self.payto_e.setCompleter(completer)
666 completer.setModel(self.completions)
668 self.message_e = QLineEdit()
669 grid.addWidget(QLabel(_('Description')), 2, 0)
670 grid.addWidget(self.message_e, 2, 1, 1, 3)
671 grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')), 2, 4)
673 self.amount_e = QLineEdit()
674 grid.addWidget(QLabel(_('Amount')), 3, 0)
675 grid.addWidget(self.amount_e, 3, 1, 1, 2)
676 grid.addWidget(HelpButton(
677 _('Amount to be sent.') + '\n\n' \
678 + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.')), 3, 3)
680 self.fee_e = QLineEdit()
681 grid.addWidget(QLabel(_('Fee')), 4, 0)
682 grid.addWidget(self.fee_e, 4, 1, 1, 2)
683 grid.addWidget(HelpButton(
684 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
685 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
686 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
688 b = EnterButton(_("Send"), self.do_send)
689 grid.addWidget(b, 6, 1)
691 b = EnterButton(_("Clear"),self.do_clear)
692 grid.addWidget(b, 6, 2)
694 self.payto_sig = QLabel('')
695 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
697 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
698 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
707 def entry_changed( is_fee ):
708 self.funds_error = False
709 amount = numbify(self.amount_e)
710 fee = numbify(self.fee_e)
711 if not is_fee: fee = None
714 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
716 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
719 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
722 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
723 self.funds_error = True
724 self.amount_e.setPalette(palette)
725 self.fee_e.setPalette(palette)
728 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
729 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
734 def update_completions(self):
736 for addr,label in self.wallet.labels.items():
737 if addr in self.wallet.addressbook:
738 l.append( label + ' <' + addr + '>')
739 l = l + self.wallet.aliases.keys()
741 self.completions.setStringList(l)
747 label = unicode( self.message_e.text() )
748 r = unicode( self.payto_e.text() )
752 m1 = re.match(ALIAS_REGEXP, r)
753 # label or alias, with address in brackets
754 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
757 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
761 to_address = m2.group(2)
765 if not self.wallet.is_valid(to_address):
766 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
770 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
772 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
775 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
777 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
780 if self.wallet.use_encryption:
781 password = self.password_dialog()
788 tx = self.wallet.mktx( [(to_address, amount)], label, password, fee)
789 except BaseException, e:
790 self.show_message(str(e))
794 h = self.wallet.send_tx(tx)
795 waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Please wait..."))
796 status, msg = self.wallet.receive_tx( h )
798 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
800 self.update_contacts_tab()
802 QMessageBox.warning(self, _('Error'), msg, _('OK'))
804 filename = 'unsigned_tx'
805 f = open(filename,'w')
808 QMessageBox.information(self, _('Unsigned transaction'), _("Unsigned transaction was saved to file:") + " " +filename, _('OK'))
811 def set_url(self, url):
812 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
813 self.tabs.setCurrentIndex(1)
814 label = self.wallet.labels.get(payto)
815 m_addr = label + ' <'+ payto+'>' if label else payto
816 self.payto_e.setText(m_addr)
818 self.message_e.setText(message)
819 self.amount_e.setText(amount)
821 self.set_frozen(self.payto_e,True)
822 self.set_frozen(self.amount_e,True)
823 self.set_frozen(self.message_e,True)
824 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
826 self.payto_sig.setVisible(False)
829 self.payto_sig.setVisible(False)
830 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
832 self.set_frozen(e,False)
834 def set_frozen(self,entry,frozen):
836 entry.setReadOnly(True)
837 entry.setFrame(False)
839 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
840 entry.setPalette(palette)
842 entry.setReadOnly(False)
845 palette.setColor(entry.backgroundRole(), QColor('white'))
846 entry.setPalette(palette)
849 def toggle_freeze(self,addr):
851 if addr in self.wallet.frozen_addresses:
852 self.wallet.unfreeze(addr)
854 self.wallet.freeze(addr)
855 self.update_receive_tab()
857 def toggle_priority(self,addr):
859 if addr in self.wallet.prioritized_addresses:
860 self.wallet.unprioritize(addr)
862 self.wallet.prioritize(addr)
863 self.update_receive_tab()
866 def create_list_tab(self, headers):
867 "generic tab creation method"
868 l = MyTreeWidget(self)
869 l.setColumnCount( len(headers) )
870 l.setHeaderLabels( headers )
880 vbox.addWidget(buttons)
885 buttons.setLayout(hbox)
890 def create_receive_tab(self):
891 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Requested'), _('Balance'), _('Tx')])
892 l.setContextMenuPolicy(Qt.CustomContextMenu)
893 l.customContextMenuRequested.connect(self.create_receive_menu)
894 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
895 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
896 self.connect(l, SIGNAL('currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)'), lambda a,b: self.recv_changed(a))
897 self.receive_list = l
898 self.receive_buttons_hbox = hbox
904 def receive_tab_set_mode(self, i):
905 self.receive_tab_mode = i
906 self.config.set_key('qt_receive_tab_mode', self.receive_tab_mode, True)
908 self.update_receive_tab()
909 self.toggle_QR_window(self.receive_tab_mode == 2)
912 def create_contacts_tab(self):
913 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
914 l.setContextMenuPolicy(Qt.CustomContextMenu)
915 l.customContextMenuRequested.connect(self.create_contact_menu)
916 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
917 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
918 self.contacts_list = l
919 self.contacts_buttons_hbox = hbox
920 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
925 def create_receive_menu(self, position):
926 # fixme: this function apparently has a side effect.
927 # if it is not called the menu pops up several times
928 #self.receive_list.selectedIndexes()
930 item = self.receive_list.itemAt(position)
932 addr = unicode(item.text(1))
934 menu.addAction(_("Copy to clipboard"), lambda: self.app.clipboard().setText(addr))
935 if self.receive_tab_mode == 2:
936 menu.addAction(_("Request amount"), lambda: self.edit_amount())
937 menu.addAction(_("View QR"), lambda: ElectrumWindow.show_qrcode("Address","bitcoin:"+addr) )
938 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
939 menu.addAction(_("Sign message"), lambda: self.sign_message(addr))
941 if self.receive_tab_mode == 1:
942 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
943 menu.addAction(t, lambda: self.toggle_freeze(addr))
944 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
945 menu.addAction(t, lambda: self.toggle_priority(addr))
947 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
950 def payto(self, x, is_alias):
957 label = self.wallet.labels.get(addr)
958 m_addr = label + ' <' + addr + '>' if label else addr
959 self.tabs.setCurrentIndex(1)
960 self.payto_e.setText(m_addr)
961 self.amount_e.setFocus()
963 def delete_contact(self, x, is_alias):
964 if self.question("Do you want to remove %s from your list of contacts?"%x):
965 if not is_alias and x in self.wallet.addressbook:
966 self.wallet.addressbook.remove(x)
967 if x in self.wallet.labels.keys():
968 self.wallet.labels.pop(x)
969 elif is_alias and x in self.wallet.aliases:
970 self.wallet.aliases.pop(x)
971 self.update_history_tab()
972 self.update_contacts_tab()
973 self.update_completions()
975 def create_contact_menu(self, position):
976 # fixme: this function apparently has a side effect.
977 # if it is not called the menu pops up several times
978 #self.contacts_list.selectedIndexes()
980 item = self.contacts_list.itemAt(position)
982 addr = unicode(item.text(0))
983 label = unicode(item.text(1))
984 is_alias = label in self.wallet.aliases.keys()
985 x = label if is_alias else addr
987 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
988 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
989 menu.addAction(_("View QR code"),lambda: self.show_qrcode("Address","bitcoin:"+addr))
991 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
993 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
994 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
995 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
998 def update_receive_item(self, item):
999 address = str( item.data(1,0).toString() )
1001 flags = self.wallet.get_address_flags(address)
1002 item.setData(0,0,flags)
1004 label = self.wallet.labels.get(address,'')
1005 item.setData(2,0,label)
1007 amount = self.wallet.requested_amounts.get(address,None)
1008 amount_str = format_satoshis( amount, False, self.wallet.num_zeros ) if amount is not None else ""
1009 item.setData(3,0,amount_str)
1011 c, u = self.wallet.get_addr_balance(address)
1012 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
1013 item.setData(4,0,balance)
1015 if self.receive_tab_mode == 1:
1016 if address in self.wallet.frozen_addresses:
1017 item.setBackgroundColor(1, QColor('lightblue'))
1018 elif address in self.wallet.prioritized_addresses:
1019 item.setBackgroundColor(1, QColor('lightgreen'))
1022 def update_receive_tab(self):
1023 l = self.receive_list
1026 l.setColumnHidden(0, not self.receive_tab_mode == 1)
1027 l.setColumnHidden(3, not self.receive_tab_mode == 2)
1028 l.setColumnHidden(4, self.receive_tab_mode == 0)
1029 l.setColumnHidden(5, not self.receive_tab_mode == 1)
1030 l.setColumnWidth(0, 50)
1031 l.setColumnWidth(1, 310)
1032 l.setColumnWidth(2, 200)
1033 l.setColumnWidth(3, 130)
1034 l.setColumnWidth(4, 130)
1035 l.setColumnWidth(5, 10)
1039 for address in self.wallet.all_addresses():
1041 if self.wallet.is_change(address) and self.receive_tab_mode != 1:
1045 h = self.wallet.history.get(address,[])
1048 for tx_hash, tx_height in h:
1049 tx = self.wallet.transactions.get(tx_hash)
1057 if address in self.wallet.addresses:
1059 if gap > self.wallet.gap_limit:
1062 if address in self.wallet.addresses:
1065 item = QTreeWidgetItem( [ '', address, '', '', '', num_tx] )
1066 item.setFont(0, QFont(MONOSPACE_FONT))
1067 item.setFont(1, QFont(MONOSPACE_FONT))
1068 item.setFont(3, QFont(MONOSPACE_FONT))
1069 self.update_receive_item(item)
1070 if is_red and address in self.wallet.addresses:
1071 item.setBackgroundColor(1, QColor('red'))
1072 l.addTopLevelItem(item)
1074 # we use column 1 because column 0 may be hidden
1075 l.setCurrentItem(l.topLevelItem(0),1)
1077 def show_contact_details(self, m):
1078 a = self.wallet.aliases.get(m)
1080 if a[0] in self.wallet.authorities.keys():
1081 s = self.wallet.authorities.get(a[0])
1084 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
1085 QMessageBox.information(self, 'Alias', msg, 'OK')
1087 def update_contacts_tab(self):
1089 l = self.contacts_list
1091 l.setColumnWidth(0, 350)
1092 l.setColumnWidth(1, 330)
1093 l.setColumnWidth(2, 100)
1096 for alias, v in self.wallet.aliases.items():
1098 alias_targets.append(target)
1099 item = QTreeWidgetItem( [ target, alias, '-'] )
1100 item.setBackgroundColor(0, QColor('lightgray'))
1101 l.addTopLevelItem(item)
1103 for address in self.wallet.addressbook:
1104 if address in alias_targets: continue
1105 label = self.wallet.labels.get(address,'')
1107 for item in self.wallet.transactions.values():
1108 if address in item['outputs'] : n=n+1
1110 item = QTreeWidgetItem( [ address, label, tx] )
1111 item.setFont(0, QFont(MONOSPACE_FONT))
1112 l.addTopLevelItem(item)
1114 l.setCurrentItem(l.topLevelItem(0))
1116 def create_wall_tab(self):
1117 self.textbox = textbox = QTextEdit(self)
1118 textbox.setFont(QFont(MONOSPACE_FONT))
1119 textbox.setReadOnly(True)
1122 def create_status_bar(self):
1124 sb.setFixedHeight(35)
1125 if self.wallet.seed:
1126 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
1127 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
1128 if self.wallet.seed:
1129 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
1130 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
1131 sb.addPermanentWidget( self.status_button )
1132 self.setStatusBar(sb)
1134 def new_contact_dialog(self):
1135 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
1136 address = unicode(text)
1138 if self.wallet.is_valid(address):
1139 self.wallet.addressbook.append(address)
1141 self.update_contacts_tab()
1142 self.update_history_tab()
1143 self.update_completions()
1145 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
1148 def show_seed_dialog(wallet, parent=None):
1150 QMessageBox.information(parent, _('Message'),
1151 _('No seed'), _('OK'))
1154 if wallet.use_encryption:
1155 password = parent.password_dialog()
1162 seed = wallet.pw_decode(wallet.seed, password)
1164 QMessageBox.warning(parent, _('Error'),
1165 _('Incorrect Password'), _('OK'))
1168 dialog = QDialog(None)
1170 dialog.setWindowTitle("Electrum")
1172 brainwallet = ' '.join(mnemonic.mn_encode(seed))
1174 msg = _("Your wallet generation seed is") +":<p>\"" + brainwallet + "\"<p>" \
1175 + _("Please write down or memorize these 12 words (order is important).") + " " \
1176 + _("This seed will allow you to recover your wallet in case of computer failure.") + "<p>" \
1177 + _("WARNING: Never disclose your seed. Never type it on a website.") + "<p>"
1179 main_text = QLabel(msg)
1180 main_text.setWordWrap(True)
1183 logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
1190 copy_function = lambda: app.clipboard().setText(brainwallet)
1191 copy_button = QPushButton(_("Copy to Clipboard"))
1192 copy_button.clicked.connect(copy_function)
1194 show_qr_function = lambda: ElectrumWindow.show_qrcode(_("Seed"), seed)
1195 qr_button = QPushButton(_("View as QR Code"))
1196 qr_button.clicked.connect(show_qr_function)
1198 ok_button = QPushButton(_("OK"))
1199 ok_button.setDefault(True)
1200 ok_button.clicked.connect(dialog.accept)
1202 main_layout = QGridLayout()
1203 main_layout.addWidget(logo, 0, 0)
1204 main_layout.addWidget(main_text, 0, 1, 1, -1)
1205 main_layout.addWidget(copy_button, 1, 1)
1206 main_layout.addWidget(qr_button, 1, 2)
1207 main_layout.addWidget(ok_button, 1, 3)
1208 dialog.setLayout(main_layout)
1213 def show_qrcode(title, data):
1217 d.setWindowTitle(title)
1218 d.setMinimumSize(270, 300)
1219 vbox = QVBoxLayout()
1220 qrw = QRCodeWidget(data)
1221 vbox.addWidget(qrw, 1)
1222 vbox.addWidget(QLabel(data), 0, Qt.AlignHCenter)
1223 hbox = QHBoxLayout()
1227 filename = "qrcode.bmp"
1228 bmp.save_qrcode(qrw.qr, filename)
1229 QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
1231 b = QPushButton(_("Print"))
1233 b.clicked.connect(print_qr)
1235 b = QPushButton(_("Close"))
1237 b.clicked.connect(d.accept)
1239 vbox.addLayout(hbox)
1243 def sign_message(self,address):
1244 if not address: return
1247 d.setWindowTitle('Sign Message')
1248 d.setMinimumSize(270, 350)
1250 tab_widget = QTabWidget()
1252 layout = QGridLayout(tab)
1254 sign_address = QLineEdit()
1255 sign_address.setText(address)
1256 layout.addWidget(QLabel(_('Address')), 1, 0)
1257 layout.addWidget(sign_address, 1, 1)
1259 sign_message = QTextEdit()
1260 layout.addWidget(QLabel(_('Message')), 2, 0)
1261 layout.addWidget(sign_message, 2, 1, 2, 1)
1263 sign_signature = QLineEdit()
1264 layout.addWidget(QLabel(_('Signature')), 3, 0)
1265 layout.addWidget(sign_signature, 3, 1)
1268 if self.wallet.use_encryption:
1269 password = self.password_dialog()
1276 signature = self.wallet.sign_message(sign_address.text(), str(sign_message.toPlainText()), password)
1277 sign_signature.setText(signature)
1278 except BaseException, e:
1279 self.show_message(str(e))
1282 hbox = QHBoxLayout()
1283 b = QPushButton(_("Sign"))
1285 b.clicked.connect(do_sign)
1286 b = QPushButton(_("Close"))
1287 b.clicked.connect(d.accept)
1289 layout.addLayout(hbox, 4, 1)
1290 tab_widget.addTab(tab, "Sign")
1294 layout = QGridLayout(tab)
1296 verify_address = QLineEdit()
1297 layout.addWidget(QLabel(_('Address')), 1, 0)
1298 layout.addWidget(verify_address, 1, 1)
1300 verify_message = QTextEdit()
1301 layout.addWidget(QLabel(_('Message')), 2, 0)
1302 layout.addWidget(verify_message, 2, 1, 2, 1)
1304 verify_signature = QLineEdit()
1305 layout.addWidget(QLabel(_('Signature')), 3, 0)
1306 layout.addWidget(verify_signature, 3, 1)
1310 self.wallet.verify_message(verify_address.text(), verify_signature.text(), str(verify_message.toPlainText()))
1311 self.show_message("Signature verified")
1312 except BaseException, e:
1313 self.show_message(str(e))
1316 hbox = QHBoxLayout()
1317 b = QPushButton(_("Verify"))
1318 b.clicked.connect(do_verify)
1320 b = QPushButton(_("Close"))
1321 b.clicked.connect(d.accept)
1323 layout.addLayout(hbox, 4, 1)
1324 tab_widget.addTab(tab, "Verify")
1326 vbox = QVBoxLayout()
1327 vbox.addWidget(tab_widget)
1332 def toggle_QR_window(self, show):
1333 if show and not self.qr_window:
1334 self.qr_window = QR_Window()
1335 self.qr_window.setVisible(True)
1336 self.qr_window_geometry = self.qr_window.geometry()
1337 item = self.receive_list.currentItem()
1339 address = str(item.text(1))
1340 label = self.wallet.labels.get(address)
1341 amount = self.wallet.requested_amounts.get(address)
1342 self.qr_window.set_content( address, label, amount )
1344 elif show and self.qr_window and not self.qr_window.isVisible():
1345 self.qr_window.setVisible(True)
1346 self.qr_window.setGeometry(self.qr_window_geometry)
1348 elif not show and self.qr_window and self.qr_window.isVisible():
1349 self.qr_window_geometry = self.qr_window.geometry()
1350 self.qr_window.setVisible(False)
1352 #self.print_button.setHidden(self.qr_window is None or not self.qr_window.isVisible())
1353 self.receive_list.setColumnHidden(3, self.qr_window is None or not self.qr_window.isVisible())
1354 self.receive_list.setColumnWidth(2, 200)
1357 def question(self, msg):
1358 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1360 def show_message(self, msg):
1361 QMessageBox.information(self, _('Message'), msg, _('OK'))
1363 def password_dialog(self ):
1370 vbox = QVBoxLayout()
1371 msg = _('Please enter your password')
1372 vbox.addWidget(QLabel(msg))
1374 grid = QGridLayout()
1376 grid.addWidget(QLabel(_('Password')), 1, 0)
1377 grid.addWidget(pw, 1, 1)
1378 vbox.addLayout(grid)
1380 vbox.addLayout(ok_cancel_buttons(d))
1383 if not d.exec_(): return
1384 return unicode(pw.text())
1391 def change_password_dialog( wallet, parent=None ):
1394 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1402 new_pw = QLineEdit()
1403 new_pw.setEchoMode(2)
1404 conf_pw = QLineEdit()
1405 conf_pw.setEchoMode(2)
1407 vbox = QVBoxLayout()
1409 msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
1410 +_('To disable wallet encryption, enter an empty new password.')) \
1411 if wallet.use_encryption else _('Your wallet keys are not encrypted')
1413 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
1414 +_("Leave these fields empty if you want to disable encryption.")
1415 vbox.addWidget(QLabel(msg))
1417 grid = QGridLayout()
1420 if wallet.use_encryption:
1421 grid.addWidget(QLabel(_('Password')), 1, 0)
1422 grid.addWidget(pw, 1, 1)
1424 grid.addWidget(QLabel(_('New Password')), 2, 0)
1425 grid.addWidget(new_pw, 2, 1)
1427 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1428 grid.addWidget(conf_pw, 3, 1)
1429 vbox.addLayout(grid)
1431 vbox.addLayout(ok_cancel_buttons(d))
1434 if not d.exec_(): return
1436 password = unicode(pw.text()) if wallet.use_encryption else None
1437 new_password = unicode(new_pw.text())
1438 new_password2 = unicode(conf_pw.text())
1441 seed = wallet.pw_decode( wallet.seed, password)
1443 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1446 if new_password != new_password2:
1447 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1448 return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
1450 wallet.update_password(seed, password, new_password)
1453 def seed_dialog(wallet, parent=None):
1457 vbox = QVBoxLayout()
1458 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1459 vbox.addWidget(QLabel(msg))
1461 grid = QGridLayout()
1464 seed_e = QLineEdit()
1465 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1466 grid.addWidget(seed_e, 1, 1)
1470 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1471 grid.addWidget(gap_e, 2, 1)
1472 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1473 vbox.addLayout(grid)
1475 vbox.addLayout(ok_cancel_buttons(d))
1478 if not d.exec_(): return
1481 gap = int(unicode(gap_e.text()))
1483 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1487 seed = unicode(seed_e.text())
1490 print_error("Warning: Not hex, trying decode")
1492 seed = mnemonic.mn_decode( seed.split(' ') )
1494 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1497 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1500 wallet.seed = str(seed)
1501 #print repr(wallet.seed)
1502 wallet.gap_limit = gap
1507 def settings_dialog(self):
1509 d.setWindowTitle(_('Electrum Settings'))
1511 vbox = QVBoxLayout()
1513 tabs = QTabWidget(self)
1514 vbox.addWidget(tabs)
1517 grid_ui = QGridLayout(tab2)
1518 grid_ui.setColumnStretch(0,1)
1519 tabs.addTab(tab2, _('Display') )
1522 grid_wallet = QGridLayout(tab)
1523 grid_wallet.setColumnStretch(0,1)
1524 tabs.addTab(tab, _('Wallet') )
1526 fee_label = QLabel(_('Transaction fee'))
1527 grid_wallet.addWidget(fee_label, 2, 0)
1529 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1530 grid_wallet.addWidget(fee_e, 2, 1)
1531 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1532 + _('Recommended value') + ': 0.001'
1533 grid_wallet.addWidget(HelpButton(msg), 2, 2)
1534 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1535 if not self.config.is_modifiable('fee'):
1536 for w in [fee_e, fee_label]: w.setEnabled(False)
1538 nz_label = QLabel(_('Display zeros'))
1539 grid_ui.addWidget(nz_label, 3, 0)
1541 nz_e.setText("%d"% self.wallet.num_zeros)
1542 grid_ui.addWidget(nz_e, 3, 1)
1543 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1544 grid_ui.addWidget(HelpButton(msg), 3, 2)
1545 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1546 if not self.config.is_modifiable('num_zeros'):
1547 for w in [nz_e, nz_label]: w.setEnabled(False)
1550 usechange_label = QLabel(_('Use change addresses'))
1551 grid_wallet.addWidget(usechange_label, 5, 0)
1552 usechange_combo = QComboBox()
1553 usechange_combo.addItems(['Yes', 'No'])
1554 usechange_combo.setCurrentIndex(not self.wallet.use_change)
1555 grid_wallet.addWidget(usechange_combo, 5, 1)
1556 grid_wallet.addWidget(HelpButton(_('Using change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1557 if not self.config.is_modifiable('use_change'): usechange_combo.setEnabled(False)
1559 gap_label = QLabel(_('Gap limit'))
1560 grid_wallet.addWidget(gap_label, 6, 0)
1562 gap_e.setText("%d"% self.wallet.gap_limit)
1563 grid_wallet.addWidget(gap_e, 6, 1)
1564 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1565 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1566 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1567 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1568 + _('Warning') + ': ' \
1569 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1570 + _('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'
1571 grid_wallet.addWidget(HelpButton(msg), 6, 2)
1572 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1573 if not self.config.is_modifiable('gap_limit'):
1574 for w in [gap_e, gap_label]: w.setEnabled(False)
1576 gui_label=QLabel(_('Default GUI') + ':')
1577 grid_ui.addWidget(gui_label , 7, 0)
1578 gui_combo = QComboBox()
1579 gui_combo.addItems(['Lite', 'Classic'])
1580 index = gui_combo.findText(self.config.get("gui","classic").capitalize())
1581 if index==-1: index = 1
1582 gui_combo.setCurrentIndex(index)
1583 grid_ui.addWidget(gui_combo, 7, 1)
1584 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)
1585 if not self.config.is_modifiable('gui'):
1586 for w in [gui_combo, gui_label]: w.setEnabled(False)
1588 lang_label=QLabel(_('Language') + ':')
1589 grid_ui.addWidget(lang_label , 8, 0)
1590 lang_combo = QComboBox()
1591 from i18n import languages
1592 lang_combo.addItems(languages.values())
1594 index = languages.keys().index(self.config.get("language",''))
1597 lang_combo.setCurrentIndex(index)
1598 grid_ui.addWidget(lang_combo, 8, 1)
1599 grid_ui.addWidget(HelpButton(_('Select which language is used in the GUI (after restart). ')), 8, 2)
1600 if not self.config.is_modifiable('language'):
1601 for w in [lang_combo, lang_label]: w.setEnabled(False)
1603 currencies = self.exchanger.get_currencies()
1604 currencies.insert(0, "None")
1606 cur_label=QLabel(_('Currency') + ':')
1607 grid_ui.addWidget(cur_label , 9, 0)
1608 cur_combo = QComboBox()
1609 cur_combo.addItems(currencies)
1611 index = currencies.index(self.config.get('currency', "None"))
1614 cur_combo.setCurrentIndex(index)
1615 grid_ui.addWidget(cur_combo, 9, 1)
1616 grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes. ')), 9, 2)
1618 view_label=QLabel(_('Receive Tab') + ':')
1619 grid_ui.addWidget(view_label , 10, 0)
1620 view_combo = QComboBox()
1621 view_combo.addItems([_('Simple'), _('Advanced'), _('Point of Sale')])
1622 view_combo.setCurrentIndex(self.receive_tab_mode)
1623 grid_ui.addWidget(view_combo, 10, 1)
1624 hh = _('This selects the interaction mode of the "Receive" tab. ') + '\n\n' \
1625 + _('Simple') + ': ' + _('Show only addresses and labels.') + '\n\n' \
1626 + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' \
1627 + _('Point of Sale') + ': ' + _('Show QR code window and amounts requested for each address. Add menu item to request amount.') + '\n\n'
1629 grid_ui.addWidget(HelpButton(hh), 10, 2)
1631 vbox.addLayout(ok_cancel_buttons(d))
1635 if not d.exec_(): return
1637 fee = unicode(fee_e.text())
1639 fee = int( 100000000 * Decimal(fee) )
1641 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1644 if self.wallet.fee != fee:
1645 self.wallet.fee = fee
1648 nz = unicode(nz_e.text())
1653 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1656 if self.wallet.num_zeros != nz:
1657 self.wallet.num_zeros = nz
1658 self.config.set_key('num_zeros', nz, True)
1659 self.update_history_tab()
1660 self.update_receive_tab()
1662 usechange_result = usechange_combo.currentIndex() == 0
1663 if self.wallet.use_change != usechange_result:
1664 self.wallet.use_change = usechange_result
1665 self.config.set_key('use_change', self.wallet.use_change, True)
1668 n = int(gap_e.text())
1670 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1673 if self.wallet.gap_limit != n:
1674 r = self.wallet.change_gap_limit(n)
1676 self.update_receive_tab()
1677 self.config.set_key('gap_limit', self.wallet.gap_limit, True)
1679 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1681 need_restart = False
1683 gui_request = str(gui_combo.currentText()).lower()
1684 if gui_request != self.config.get('gui'):
1685 self.config.set_key('gui', gui_request, True)
1688 lang_request = languages.keys()[lang_combo.currentIndex()]
1689 if lang_request != self.config.get('language'):
1690 self.config.set_key("language", lang_request, True)
1693 cur_request = str(currencies[cur_combo.currentIndex()])
1694 if cur_request != self.config.get('currency', "None"):
1695 self.config.set_key('currency', cur_request, True)
1696 self.update_wallet()
1699 QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
1701 self.receive_tab_set_mode(view_combo.currentIndex())
1705 def network_dialog(wallet, parent=None):
1706 interface = wallet.interface
1708 if interface.is_connected:
1709 status = _("Connected to")+" %s\n%d blocks"%(interface.host, wallet.verifier.height)
1711 status = _("Not connected")
1712 server = interface.server
1715 status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
1716 server = interface.server
1718 plist, servers_list = interface.get_servers_list()
1722 d.setWindowTitle(_('Server'))
1723 d.setMinimumSize(375, 20)
1725 vbox = QVBoxLayout()
1728 hbox = QHBoxLayout()
1730 l.setPixmap(QPixmap(":icons/network.png"))
1733 hbox.addWidget(QLabel(status))
1735 vbox.addLayout(hbox)
1739 grid = QGridLayout()
1741 vbox.addLayout(grid)
1744 server_protocol = QComboBox()
1745 server_host = QLineEdit()
1746 server_host.setFixedWidth(200)
1747 server_port = QLineEdit()
1748 server_port.setFixedWidth(60)
1750 protocol_names = ['TCP', 'HTTP', 'TCP/SSL', 'HTTPS']
1751 protocol_letters = 'thsg'
1752 DEFAULT_PORTS = {'t':'50001', 's':'50002', 'h':'8081', 'g':'8082'}
1753 server_protocol.addItems(protocol_names)
1755 grid.addWidget(QLabel(_('Server') + ':'), 0, 0)
1756 grid.addWidget(server_protocol, 0, 1)
1757 grid.addWidget(server_host, 0, 2)
1758 grid.addWidget(server_port, 0, 3)
1760 def change_protocol(p):
1761 protocol = protocol_letters[p]
1762 host = unicode(server_host.text())
1763 pp = plist.get(host,DEFAULT_PORTS)
1764 if protocol not in pp.keys():
1765 protocol = pp.keys()[0]
1767 server_host.setText( host )
1768 server_port.setText( port )
1770 server_protocol.connect(server_protocol, SIGNAL('currentIndexChanged(int)'), change_protocol)
1772 label = _('Active Servers') if wallet.interface.servers else _('Default Servers')
1773 servers_list_widget = QTreeWidget(parent)
1774 servers_list_widget.setHeaderLabels( [ label, _('Type') ] )
1775 servers_list_widget.setMaximumHeight(150)
1776 servers_list_widget.setColumnWidth(0, 240)
1777 for _host in servers_list.keys():
1778 _type = 'P' if servers_list[_host].get('pruning') else 'F'
1779 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ _host, _type ] ))
1781 def change_server(host, protocol=None):
1782 pp = plist.get(host,DEFAULT_PORTS)
1784 port = pp.get(protocol)
1785 if not port: protocol = None
1788 if 't' in pp.keys():
1790 port = pp.get(protocol)
1792 protocol = pp.keys()[0]
1793 port = pp.get(protocol)
1795 server_host.setText( host )
1796 server_port.setText( port )
1797 server_protocol.setCurrentIndex(protocol_letters.index(protocol))
1799 if not plist: return
1800 for p in protocol_letters:
1801 i = protocol_letters.index(p)
1802 j = server_protocol.model().index(i,0)
1803 if p not in pp.keys():
1804 server_protocol.model().setData(j, QtCore.QVariant(0), QtCore.Qt.UserRole-1)
1806 server_protocol.model().setData(j, QtCore.QVariant(0,False), QtCore.Qt.UserRole-1)
1810 host, port, protocol = server.split(':')
1811 change_server(host,protocol)
1813 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda x: change_server(unicode(x.text(0))))
1814 grid.addWidget(servers_list_widget, 1, 1, 1, 3)
1816 if not wallet.config.is_modifiable('server'):
1817 for w in [server_host, server_port, server_protocol, servers_list_widget]: w.setEnabled(False)
1820 autocycle_cb = QCheckBox('Try random servers if disconnected')
1821 autocycle_cb.setChecked(wallet.config.get('auto_cycle', False))
1822 grid.addWidget(autocycle_cb, 3, 1, 3, 2)
1823 if not wallet.config.is_modifiable('auto_cycle'): autocycle_cb.setEnabled(False)
1826 proxy_mode = QComboBox()
1827 proxy_host = QLineEdit()
1828 proxy_host.setFixedWidth(200)
1829 proxy_port = QLineEdit()
1830 proxy_port.setFixedWidth(60)
1831 proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1833 def check_for_disable(index = False):
1834 if proxy_mode.currentText() != 'NONE':
1835 proxy_host.setEnabled(True)
1836 proxy_port.setEnabled(True)
1838 proxy_host.setEnabled(False)
1839 proxy_port.setEnabled(False)
1842 proxy_mode.connect(proxy_mode, SIGNAL('currentIndexChanged(int)'), check_for_disable)
1844 if not wallet.config.is_modifiable('proxy'):
1845 for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
1847 proxy_config = interface.proxy if interface.proxy else { "mode":"none", "host":"localhost", "port":"8080"}
1848 proxy_mode.setCurrentIndex(proxy_mode.findText(str(proxy_config.get("mode").upper())))
1849 proxy_host.setText(proxy_config.get("host"))
1850 proxy_port.setText(proxy_config.get("port"))
1852 grid.addWidget(QLabel(_('Proxy') + ':'), 2, 0)
1853 grid.addWidget(proxy_mode, 2, 1)
1854 grid.addWidget(proxy_host, 2, 2)
1855 grid.addWidget(proxy_port, 2, 3)
1858 vbox.addLayout(ok_cancel_buttons(d))
1861 if not d.exec_(): return
1863 server = unicode( server_host.text() ) + ':' + unicode( server_port.text() ) + ':' + (protocol_letters[server_protocol.currentIndex()])
1864 if proxy_mode.currentText() != 'NONE':
1865 proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1869 wallet.config.set_key("proxy", proxy, True)
1870 wallet.config.set_key("server", server, True)
1871 interface.set_server(server, proxy)
1872 wallet.config.set_key('auto_cycle', autocycle_cb.isChecked(), True)
1875 def closeEvent(self, event):
1877 self.config.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()], True)
1883 def __init__(self, wallet, config, app=None):
1884 self.wallet = wallet
1885 self.config = config
1887 self.app = QApplication(sys.argv)
1890 def restore_or_create(self):
1891 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1892 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1893 if r==2: return None
1894 return 'restore' if r==1 else 'create'
1896 def seed_dialog(self):
1897 return ElectrumWindow.seed_dialog( self.wallet )
1899 def network_dialog(self):
1900 return ElectrumWindow.network_dialog( self.wallet, parent=None )
1903 def show_seed(self):
1904 ElectrumWindow.show_seed_dialog(self.wallet)
1907 def password_dialog(self):
1908 ElectrumWindow.change_password_dialog(self.wallet)
1911 def restore_wallet(self):
1912 wallet = self.wallet
1913 # wait until we are connected, because the user might have selected another server
1914 if not wallet.interface.is_connected:
1915 waiting = lambda: False if wallet.interface.is_connected else "connecting...\n"
1916 waiting_dialog(waiting)
1918 waiting = lambda: False if wallet.is_up_to_date() else "Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1919 %(len(wallet.all_addresses()), wallet.interface.bytes_received/1024.)
1921 wallet.set_up_to_date(False)
1922 wallet.interface.poke('synchronizer')
1923 waiting_dialog(waiting)
1924 if wallet.is_found():
1925 print_error( "Recovery successful" )
1927 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1934 w = ElectrumWindow(self.wallet, self.config)
1935 if url: w.set_url(url)