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
41 from simple_config import SimpleConfig
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, addr):
142 super(QRCodeWidget, self).__init__()
143 self.setGeometry(300, 300, 350, 350)
146 def set_addr(self, addr):
148 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
149 self.qr.addData(addr)
152 def paintEvent(self, e):
153 qp = QtGui.QPainter()
156 size = self.qr.getModuleCount()*boxsize
157 k = self.qr.getModuleCount()
158 black = QColor(0, 0, 0, 255)
159 white = QColor(255, 255, 255, 255)
162 if self.qr.isDark(r, c):
168 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
173 def ok_cancel_buttons(dialog):
176 b = QPushButton("OK")
178 b.clicked.connect(dialog.accept)
179 b = QPushButton("Cancel")
181 b.clicked.connect(dialog.reject)
185 class ElectrumWindow(QMainWindow):
187 def __init__(self, wallet):
188 QMainWindow.__init__(self)
190 self.wallet.register_callback(self.update_callback)
192 self.funds_error = False
193 self.completions = QStringListModel()
195 self.tabs = tabs = QTabWidget(self)
196 tabs.addTab(self.create_history_tab(), _('History') )
198 tabs.addTab(self.create_send_tab(), _('Send') )
199 tabs.addTab(self.create_receive_tab(), _('Receive') )
200 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
201 tabs.addTab(self.create_wall_tab(), _('Wall') )
202 tabs.setMinimumSize(600, 400)
203 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
204 self.setCentralWidget(tabs)
205 self.create_status_bar()
207 g = cfg.config["winpos-qt"]
208 self.setGeometry(g[0], g[1], g[2], g[3])
209 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
210 if not self.wallet.seed: title += ' [seedless]'
211 self.setWindowTitle( title )
213 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
214 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
215 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
216 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
218 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
219 self.history_list.setFocus(True)
221 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
222 if platform.system() == 'Windows':
223 n = 3 if self.wallet.seed else 2
224 tabs.setCurrentIndex (n)
225 tabs.setCurrentIndex (0)
228 def connect_slots(self, sender):
230 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
231 self.previous_payto_e=''
233 def check_recipient(self):
234 if self.payto_e.hasFocus():
236 r = unicode( self.payto_e.text() )
237 if r != self.previous_payto_e:
238 self.previous_payto_e = r
240 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
242 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
246 s = r + ' <' + to_address + '>'
247 self.payto_e.setText(s)
250 def update_callback(self):
251 self.emit(QtCore.SIGNAL('updatesignal'))
253 def update_wallet(self):
254 if self.wallet.interface and self.wallet.interface.is_connected:
255 if self.wallet.blocks == -1:
256 text = _( "Connecting..." )
257 icon = QIcon(":icons/status_disconnected.png")
258 elif self.wallet.blocks == 0:
259 text = _( "Server not ready" )
260 icon = QIcon(":icons/status_disconnected.png")
261 elif not self.wallet.up_to_date:
262 text = _( "Synchronizing..." )
263 icon = QIcon(":icons/status_waiting.png")
265 c, u = self.wallet.get_balance()
266 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
267 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
268 icon = QIcon(":icons/status_connected.png")
270 text = _( "Not connected" )
271 icon = QIcon(":icons/status_disconnected.png")
274 text = _( "Not enough funds" )
276 self.statusBar().showMessage(text)
277 self.status_button.setIcon( icon )
279 if self.wallet.up_to_date:
280 self.textbox.setText( self.wallet.banner )
281 self.update_history_tab()
282 self.update_receive_tab()
283 self.update_contacts_tab()
284 self.update_completions()
287 def create_history_tab(self):
288 self.history_list = l = MyTreeWidget(self)
290 l.setColumnWidth(0, 40)
291 l.setColumnWidth(1, 140)
292 l.setColumnWidth(2, 350)
293 l.setColumnWidth(3, 140)
294 l.setColumnWidth(4, 140)
295 l.setHeaderLabels( [ '', _( 'Date' ), _( 'To / From' ) , _('Amount'), _('Balance')] )
296 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
297 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
298 l.setContextMenuPolicy(Qt.CustomContextMenu)
299 l.customContextMenuRequested.connect(self.create_history_menu)
302 def create_history_menu(self, position):
303 self.history_list.selectedIndexes()
304 item = self.history_list.currentItem()
306 tx_hash = str(item.toolTip(0))
308 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
309 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
310 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
311 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
313 def tx_details(self, tx_hash):
314 tx = self.wallet.tx_history.get(tx_hash)
317 conf = self.wallet.blocks - tx['height'] + 1
318 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
323 tx_details = _("Transaction Details") +"\n\n" \
324 + "Transaction ID:\n" + tx_hash + "\n\n" \
325 + "Status: %d confirmations\n\n"%conf \
326 + "Date: %s\n\n"%time_str \
327 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
328 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
330 r = self.wallet.receipts.get(tx_hash)
332 tx_details += "\n_______________________________________" \
333 + '\n\nSigned URI: ' + r[2] \
334 + "\n\nSigned by: " + r[0] \
335 + '\n\nSignature: ' + r[1]
337 QMessageBox.information(self, 'Details', tx_details, 'OK')
340 def tx_label_clicked(self, item, column):
341 if column==2 and item.isSelected():
342 tx_hash = str(item.toolTip(0))
344 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
345 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
346 self.history_list.editItem( item, column )
347 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
350 def tx_label_changed(self, item, column):
354 tx_hash = str(item.toolTip(0))
355 tx = self.wallet.tx_history.get(tx_hash)
356 s = self.wallet.labels.get(tx_hash)
357 text = unicode( item.text(2) )
359 self.wallet.labels[tx_hash] = text
360 item.setForeground(2, QBrush(QColor('black')))
362 if s: self.wallet.labels.pop(tx_hash)
363 text = tx['default_label']
364 item.setText(2, text)
365 item.setForeground(2, QBrush(QColor('gray')))
368 def edit_label(self, is_recv):
369 l = self.receive_list if is_recv else self.contacts_list
370 c = 2 if is_recv else 1
371 item = l.currentItem()
372 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
373 l.editItem( item, c )
374 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
376 def address_label_clicked(self, item, column, l, column_addr, column_label):
377 if column==column_label and item.isSelected():
378 addr = unicode( item.text(column_addr) )
379 label = unicode( item.text(column_label) )
380 if label in self.wallet.aliases.keys():
382 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
383 l.editItem( item, column )
384 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
386 def address_label_changed(self, item, column, l, column_addr, column_label):
387 addr = unicode( item.text(column_addr) )
388 text = unicode( item.text(column_label) )
390 if text not in self.wallet.aliases.keys():
391 self.wallet.labels[addr] = text
393 print_error("Error: This is one of your aliases")
394 label = self.wallet.labels.get(addr,'')
395 item.setText(column_label, QString(label))
397 s = self.wallet.labels.get(addr)
398 if s: self.wallet.labels.pop(addr)
400 self.update_history_tab()
401 self.update_completions()
403 def update_history_tab(self):
404 self.history_list.clear()
406 for tx in self.wallet.get_tx_history():
407 tx_hash = tx['tx_hash']
409 conf = self.wallet.blocks - tx['height'] + 1
410 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
411 icon = QIcon(":icons/confirmed.png")
415 icon = QIcon(":icons/unconfirmed.png")
418 label = self.wallet.labels.get(tx_hash)
419 is_default_label = (label == '') or (label is None)
420 if is_default_label: label = tx['default_label']
422 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
423 item.setFont(2, QFont(MONOSPACE_FONT))
424 item.setFont(3, QFont(MONOSPACE_FONT))
425 item.setFont(4, QFont(MONOSPACE_FONT))
426 item.setToolTip(0, tx_hash)
428 item.setForeground(2, QBrush(QColor('grey')))
430 item.setIcon(0, icon)
431 self.history_list.insertTopLevelItem(0,item)
433 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
436 def create_send_tab(self):
441 grid.setColumnMinimumWidth(3,300)
442 grid.setColumnStretch(5,1)
444 self.payto_e = QLineEdit()
445 grid.addWidget(QLabel(_('Pay to')), 1, 0)
446 grid.addWidget(self.payto_e, 1, 1, 1, 3)
449 qrcode = qrscanner.scan_qr()
450 if 'address' in qrcode:
451 self.payto_e.setText(qrcode['address'])
452 if 'amount' in qrcode:
453 self.amount_e.setText(str(qrcode['amount']))
454 if 'label' in qrcode:
455 self.message_e.setText(qrcode['label'])
456 if 'message' in qrcode:
457 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
460 if qrscanner.is_available():
461 b = QPushButton(_("Scan QR code"))
462 b.clicked.connect(fill_from_qr)
463 grid.addWidget(b, 1, 5)
465 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)
467 completer = QCompleter()
468 completer.setCaseSensitivity(False)
469 self.payto_e.setCompleter(completer)
470 completer.setModel(self.completions)
472 self.message_e = QLineEdit()
473 grid.addWidget(QLabel(_('Description')), 2, 0)
474 grid.addWidget(self.message_e, 2, 1, 1, 3)
475 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)
477 self.amount_e = QLineEdit()
478 grid.addWidget(QLabel(_('Amount')), 3, 0)
479 grid.addWidget(self.amount_e, 3, 1, 1, 2)
480 grid.addWidget(HelpButton(
481 _('Amount to be sent.') + '\n\n' \
482 + _('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)
484 self.fee_e = QLineEdit()
485 grid.addWidget(QLabel(_('Fee')), 4, 0)
486 grid.addWidget(self.fee_e, 4, 1, 1, 2)
487 grid.addWidget(HelpButton(
488 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
489 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
490 + _('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)
492 b = EnterButton(_("Send"), self.do_send)
493 grid.addWidget(b, 6, 1)
495 b = EnterButton(_("Clear"),self.do_clear)
496 grid.addWidget(b, 6, 2)
498 self.payto_sig = QLabel('')
499 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
501 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
502 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
511 def entry_changed( is_fee ):
512 self.funds_error = False
513 amount = numbify(self.amount_e)
514 fee = numbify(self.fee_e)
515 if not is_fee: fee = None
518 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
520 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
523 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
526 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
527 self.funds_error = True
528 self.amount_e.setPalette(palette)
529 self.fee_e.setPalette(palette)
531 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
532 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
537 def update_completions(self):
539 for addr,label in self.wallet.labels.items():
540 if addr in self.wallet.addressbook:
541 l.append( label + ' <' + addr + '>')
542 l = l + self.wallet.aliases.keys()
544 self.completions.setStringList(l)
550 label = unicode( self.message_e.text() )
551 r = unicode( self.payto_e.text() )
555 m1 = re.match(ALIAS_REGEXP, r)
556 # label or alias, with address in brackets
557 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
560 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
564 to_address = m2.group(2)
568 if not self.wallet.is_valid(to_address):
569 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
573 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
575 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
578 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
580 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
583 if self.wallet.use_encryption:
584 password = self.password_dialog()
591 tx = self.wallet.mktx( to_address, amount, label, password, fee)
592 except BaseException, e:
593 self.show_message(str(e))
596 status, msg = self.wallet.sendtx( tx )
598 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
600 self.update_contacts_tab()
602 QMessageBox.warning(self, _('Error'), msg, _('OK'))
605 def set_url(self, url):
606 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
607 self.tabs.setCurrentIndex(1)
608 label = self.wallet.labels.get(payto)
609 m_addr = label + ' <'+ payto+'>' if label else payto
610 self.payto_e.setText(m_addr)
612 self.message_e.setText(message)
613 self.amount_e.setText(amount)
615 self.set_frozen(self.payto_e,True)
616 self.set_frozen(self.amount_e,True)
617 self.set_frozen(self.message_e,True)
618 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
620 self.payto_sig.setVisible(False)
623 self.payto_sig.setVisible(False)
624 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
626 self.set_frozen(e,False)
628 def set_frozen(self,entry,frozen):
630 entry.setReadOnly(True)
631 entry.setFrame(False)
633 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
634 entry.setPalette(palette)
636 entry.setReadOnly(False)
639 palette.setColor(entry.backgroundRole(), QColor('white'))
640 entry.setPalette(palette)
643 def toggle_freeze(self,addr):
645 if addr in self.wallet.frozen_addresses:
646 self.wallet.unfreeze(addr)
648 self.wallet.freeze(addr)
649 self.update_receive_tab()
651 def toggle_priority(self,addr):
653 if addr in self.wallet.prioritized_addresses:
654 self.wallet.unprioritize(addr)
656 self.wallet.prioritize(addr)
657 self.update_receive_tab()
660 def create_list_tab(self, headers):
661 "generic tab creation method"
662 l = MyTreeWidget(self)
663 l.setColumnCount( len(headers) )
664 l.setHeaderLabels( headers )
674 vbox.addWidget(buttons)
679 buttons.setLayout(hbox)
684 def create_receive_tab(self):
685 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
686 l.setContextMenuPolicy(Qt.CustomContextMenu)
687 l.customContextMenuRequested.connect(self.create_receive_menu)
688 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
689 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
690 self.receive_list = l
691 self.receive_buttons_hbox = hbox
695 def create_contacts_tab(self):
696 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
697 l.setContextMenuPolicy(Qt.CustomContextMenu)
698 l.customContextMenuRequested.connect(self.create_contact_menu)
699 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
700 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
701 self.contacts_list = l
702 self.contacts_buttons_hbox = hbox
703 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
708 def create_receive_menu(self, position):
709 # fixme: this function apparently has a side effect.
710 # if it is not called the menu pops up several times
711 #self.receive_list.selectedIndexes()
713 item = self.receive_list.itemAt(position)
715 addr = unicode(item.text(1))
717 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
718 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
719 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
720 if self.wallet.expert_mode:
721 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
722 menu.addAction(t, lambda: self.toggle_freeze(addr))
723 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
724 menu.addAction(t, lambda: self.toggle_priority(addr))
725 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
728 def payto(self, x, is_alias):
735 label = self.wallet.labels.get(addr)
736 m_addr = label + ' <' + addr + '>' if label else addr
737 self.tabs.setCurrentIndex(1)
738 self.payto_e.setText(m_addr)
739 self.amount_e.setFocus()
741 def delete_contact(self, x, is_alias):
742 if self.question("Do you want to remove %s from your list of contacts?"%x):
743 if not is_alias and x in self.wallet.addressbook:
744 self.wallet.addressbook.remove(x)
745 if x in self.wallet.labels.keys():
746 self.wallet.labels.pop(x)
747 elif is_alias and x in self.wallet.aliases:
748 self.wallet.aliases.pop(x)
749 self.update_history_tab()
750 self.update_contacts_tab()
751 self.update_completions()
753 def create_contact_menu(self, position):
754 # fixme: this function apparently has a side effect.
755 # if it is not called the menu pops up several times
756 #self.contacts_list.selectedIndexes()
758 item = self.contacts_list.itemAt(position)
760 addr = unicode(item.text(0))
761 label = unicode(item.text(1))
762 is_alias = label in self.wallet.aliases.keys()
763 x = label if is_alias else addr
765 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
766 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
767 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
769 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
771 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
772 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
773 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
776 def update_receive_tab(self):
777 l = self.receive_list
779 l.setColumnHidden(0,not self.wallet.expert_mode)
780 l.setColumnHidden(3,not self.wallet.expert_mode)
781 l.setColumnHidden(4,not self.wallet.expert_mode)
782 l.setColumnWidth(0, 50)
783 l.setColumnWidth(1, 310)
784 l.setColumnWidth(2, 250)
785 l.setColumnWidth(3, 130)
786 l.setColumnWidth(4, 10)
790 for address in self.wallet.all_addresses():
792 if self.wallet.is_change(address) and not self.wallet.expert_mode:
795 label = self.wallet.labels.get(address,'')
797 h = self.wallet.history.get(address,[])
799 if not item['is_input'] : n=n+1
803 if address in self.wallet.addresses:
805 if gap > self.wallet.gap_limit:
808 if address in self.wallet.addresses:
811 c, u = self.wallet.get_addr_balance(address)
812 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
813 flags = self.wallet.get_address_flags(address)
814 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
816 item.setFont(0, QFont(MONOSPACE_FONT))
817 item.setFont(1, QFont(MONOSPACE_FONT))
818 item.setFont(3, QFont(MONOSPACE_FONT))
819 if address in self.wallet.frozen_addresses:
820 item.setBackgroundColor(1, QColor('lightblue'))
821 elif address in self.wallet.prioritized_addresses:
822 item.setBackgroundColor(1, QColor('lightgreen'))
823 if is_red and address in self.wallet.addresses:
824 item.setBackgroundColor(1, QColor('red'))
825 l.addTopLevelItem(item)
827 # we use column 1 because column 0 may be hidden
828 l.setCurrentItem(l.topLevelItem(0),1)
830 def show_contact_details(self, m):
831 a = self.wallet.aliases.get(m)
833 if a[0] in self.wallet.authorities.keys():
834 s = self.wallet.authorities.get(a[0])
837 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
838 QMessageBox.information(self, 'Alias', msg, 'OK')
840 def update_contacts_tab(self):
842 l = self.contacts_list
844 l.setColumnHidden(2, not self.wallet.expert_mode)
845 l.setColumnWidth(0, 350)
846 l.setColumnWidth(1, 330)
847 l.setColumnWidth(2, 100)
850 for alias, v in self.wallet.aliases.items():
852 alias_targets.append(target)
853 item = QTreeWidgetItem( [ target, alias, '-'] )
854 item.setBackgroundColor(0, QColor('lightgray'))
855 l.addTopLevelItem(item)
857 for address in self.wallet.addressbook:
858 if address in alias_targets: continue
859 label = self.wallet.labels.get(address,'')
861 for item in self.wallet.tx_history.values():
862 if address in item['outputs'] : n=n+1
864 item = QTreeWidgetItem( [ address, label, tx] )
865 item.setFont(0, QFont(MONOSPACE_FONT))
866 l.addTopLevelItem(item)
868 l.setCurrentItem(l.topLevelItem(0))
870 def create_wall_tab(self):
871 self.textbox = textbox = QTextEdit(self)
872 textbox.setFont(QFont(MONOSPACE_FONT))
873 textbox.setReadOnly(True)
876 def create_status_bar(self):
878 sb.setFixedHeight(35)
880 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
881 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
883 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
884 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
885 sb.addPermanentWidget( self.status_button )
886 self.setStatusBar(sb)
888 def new_contact_dialog(self):
889 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
890 address = unicode(text)
892 if self.wallet.is_valid(address):
893 self.wallet.addressbook.append(address)
895 self.update_contacts_tab()
896 self.update_history_tab()
897 self.update_completions()
899 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
902 def show_seed_dialog(wallet, parent=None):
904 QMessageBox.information(parent, _('Message'),
905 _('No seed'), _('OK'))
908 if wallet.use_encryption:
909 password = parent.password_dialog()
916 seed = wallet.pw_decode(wallet.seed, password)
918 QMessageBox.warning(parent, _('Error'),
919 _('Incorrect Password'), _('OK'))
922 dialog = QDialog(None)
924 dialog.setWindowTitle(_("Seed"))
926 brainwallet = ' '.join(mnemonic.mn_encode(seed))
928 msg = _('<p>"%s"</p>'
929 "<p>If you memorise or write down these 12 words, you will always be able to recover your wallet.</p>"
930 "<p>This is called a 'BrainWallet'. The order of words is important. Case does not matter (capitals or lowercase).</p>") % brainwallet
931 main_text = QLabel(msg)
932 main_text.setWordWrap(True)
935 logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
942 copy_function = lambda: app.clipboard().setText(brainwallet)
943 copy_button = QPushButton(_("Copy to Clipboard"))
944 copy_button.clicked.connect(copy_function)
946 show_qr_function = lambda: ElectrumWindow.show_seed_qrcode(seed)
947 qr_button = QPushButton(_("View as QR Code"))
948 qr_button.clicked.connect(show_qr_function)
950 ok_button = QPushButton(_("OK"))
951 ok_button.clicked.connect(dialog.accept)
953 main_layout = QGridLayout()
954 main_layout.addWidget(logo, 0, 0)
955 main_layout.addWidget(main_text, 0, 1, 1, -1)
956 main_layout.addWidget(copy_button, 1, 1)
957 main_layout.addWidget(qr_button, 1, 2)
958 main_layout.addWidget(ok_button, 1, 3)
959 dialog.setLayout(main_layout)
964 def show_seed_qrcode(seed):
968 d.setWindowTitle(_("Seed"))
969 d.setMinimumSize(270, 300)
971 vbox.addWidget(QRCodeWidget(seed))
974 b = QPushButton(_("OK"))
976 b.clicked.connect(d.accept)
983 def show_address_qrcode(self,address):
984 if not address: return
987 d.setWindowTitle(address)
988 d.setMinimumSize(270, 350)
990 qrw = QRCodeWidget(address)
994 amount_e = QLineEdit()
995 hbox.addWidget(QLabel(_('Amount')))
996 hbox.addWidget(amount_e)
999 #hbox = QHBoxLayout()
1000 #label_e = QLineEdit()
1001 #hbox.addWidget(QLabel('Label'))
1002 #hbox.addWidget(label_e)
1003 #vbox.addLayout(hbox)
1005 def amount_changed():
1006 amount = numbify(amount_e)
1007 #label = str( label_e.getText() )
1008 if amount is not None:
1009 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
1011 qrw.set_addr( address )
1015 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
1016 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
1018 amount_e.textChanged.connect( amount_changed )
1020 hbox = QHBoxLayout()
1022 b = QPushButton(_("Save"))
1023 b.clicked.connect(do_save)
1025 b = QPushButton(_("Close"))
1027 b.clicked.connect(d.accept)
1029 vbox.addLayout(hbox)
1033 def question(self, msg):
1034 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1036 def show_message(self, msg):
1037 QMessageBox.information(self, _('Message'), msg, _('OK'))
1039 def password_dialog(self ):
1046 vbox = QVBoxLayout()
1047 msg = _('Please enter your password')
1048 vbox.addWidget(QLabel(msg))
1050 grid = QGridLayout()
1052 grid.addWidget(QLabel(_('Password')), 1, 0)
1053 grid.addWidget(pw, 1, 1)
1054 vbox.addLayout(grid)
1056 vbox.addLayout(ok_cancel_buttons(d))
1059 if not d.exec_(): return
1060 return unicode(pw.text())
1067 def change_password_dialog( wallet, parent=None ):
1070 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1078 new_pw = QLineEdit()
1079 new_pw.setEchoMode(2)
1080 conf_pw = QLineEdit()
1081 conf_pw.setEchoMode(2)
1083 vbox = QVBoxLayout()
1085 msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'+_('To disable wallet encryption, enter an empty new password.')) if wallet.use_encryption else _('Your wallet keys are not encrypted')
1087 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1088 vbox.addWidget(QLabel(msg))
1090 grid = QGridLayout()
1093 if wallet.use_encryption:
1094 grid.addWidget(QLabel(_('Password')), 1, 0)
1095 grid.addWidget(pw, 1, 1)
1097 grid.addWidget(QLabel(_('New Password')), 2, 0)
1098 grid.addWidget(new_pw, 2, 1)
1100 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1101 grid.addWidget(conf_pw, 3, 1)
1102 vbox.addLayout(grid)
1104 vbox.addLayout(ok_cancel_buttons(d))
1107 if not d.exec_(): return
1109 password = unicode(pw.text()) if wallet.use_encryption else None
1110 new_password = unicode(new_pw.text())
1111 new_password2 = unicode(conf_pw.text())
1114 seed = wallet.pw_decode( wallet.seed, password)
1116 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1119 if new_password != new_password2:
1120 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1123 wallet.update_password(seed, password, new_password)
1126 def seed_dialog(wallet, parent=None):
1130 vbox = QVBoxLayout()
1131 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1132 vbox.addWidget(QLabel(msg))
1134 grid = QGridLayout()
1137 seed_e = QLineEdit()
1138 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1139 grid.addWidget(seed_e, 1, 1)
1143 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1144 grid.addWidget(gap_e, 2, 1)
1145 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1146 vbox.addLayout(grid)
1148 vbox.addLayout(ok_cancel_buttons(d))
1151 if not d.exec_(): return
1154 gap = int(unicode(gap_e.text()))
1156 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1160 seed = unicode(seed_e.text())
1163 print_error("Warning: Not hex, trying decode")
1165 seed = mnemonic.mn_decode( seed.split(' ') )
1167 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1170 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1173 wallet.seed = str(seed)
1174 #print repr(wallet.seed)
1175 wallet.gap_limit = gap
1179 def set_expert_mode(self, b):
1180 self.wallet.expert_mode = b
1182 self.update_receive_tab()
1183 self.update_contacts_tab()
1184 # if self.wallet.seed:
1185 # self.nochange_cb.setHidden(not self.wallet.expert_mode)
1188 def settings_dialog(self):
1191 vbox = QVBoxLayout()
1192 msg = _('Here are the settings of your wallet.') + '\n'\
1193 + _('For more explanations, click on the help buttons next to each field.')
1196 label.setFixedWidth(250)
1197 label.setWordWrap(True)
1198 label.setAlignment(Qt.AlignJustify)
1199 vbox.addWidget(label)
1201 grid = QGridLayout()
1203 vbox.addLayout(grid)
1206 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1207 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1208 grid.addWidget(fee_e, 2, 1)
1209 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1210 + _('Recommended value') + ': 0.001'
1211 grid.addWidget(HelpButton(msg), 2, 2)
1212 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1215 nz_e.setText("%d"% self.wallet.num_zeros)
1216 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1217 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1218 grid.addWidget(HelpButton(msg), 3, 2)
1219 grid.addWidget(nz_e, 3, 1)
1220 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1222 cb = QCheckBox(_('Expert mode'))
1223 grid.addWidget(cb, 4, 0)
1224 cb.setChecked(self.wallet.expert_mode)
1226 if self.wallet.expert_mode:
1228 usechange_cb = QCheckBox(_('Use change addresses'))
1229 grid.addWidget(usechange_cb, 5, 0)
1230 usechange_cb.setChecked(self.wallet.use_change)
1231 grid.addWidget(HelpButton(_('Using a change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1233 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1234 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1235 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1236 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1237 + _('Warning') + ': ' \
1238 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1239 + _('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'
1241 gap_e.setText("%d"% self.wallet.gap_limit)
1242 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1243 grid.addWidget(gap_e, 6, 1)
1244 grid.addWidget(HelpButton(msg), 6, 2)
1245 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1248 vbox.addLayout(ok_cancel_buttons(d))
1252 if not d.exec_(): return
1254 fee = unicode(fee_e.text())
1256 fee = int( 100000000 * Decimal(fee) )
1258 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1261 if self.wallet.fee != fee:
1262 self.wallet.fee = fee
1265 nz = unicode(nz_e.text())
1270 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1273 if self.wallet.num_zeros != nz:
1274 self.wallet.num_zeros = nz
1275 self.update_history_tab()
1276 self.update_receive_tab()
1279 if self.wallet.expert_mode:
1281 self.wallet.use_change = usechange_cb.isChecked()
1284 n = int(gap_e.text())
1286 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1288 if self.wallet.gap_limit != n:
1289 r = self.wallet.change_gap_limit(n)
1291 self.update_receive_tab()
1293 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1295 self.set_expert_mode(cb.isChecked())
1299 def network_dialog(wallet, parent=None):
1300 interface = wallet.interface
1302 if interface.is_connected:
1303 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1305 status = _("Not connected")
1306 server = wallet.server
1309 status = _("Please choose a server.")
1310 server = random.choice( DEFAULT_SERVERS )
1312 if not wallet.interface.servers:
1314 for x in DEFAULT_SERVERS:
1315 h,port,protocol = x.split(':')
1316 servers_list.append( (h,[(protocol,port)] ) )
1318 servers_list = wallet.interface.servers
1321 for item in servers_list:
1325 protocol, port = item2
1331 d.setWindowTitle(_('Server'))
1332 d.setMinimumSize(375, 20)
1334 vbox = QVBoxLayout()
1337 hbox = QHBoxLayout()
1339 l.setPixmap(QPixmap(":icons/network.png"))
1341 hbox.addWidget(QLabel(status))
1343 vbox.addLayout(hbox)
1345 hbox = QHBoxLayout()
1346 host_line = QLineEdit()
1347 host_line.setText(server)
1348 hbox.addWidget(QLabel(_('Connect to') + ':'))
1349 hbox.addWidget(host_line)
1350 vbox.addLayout(hbox)
1352 hbox = QHBoxLayout()
1354 buttonGroup = QGroupBox(_("Protocol"))
1355 radio1 = QRadioButton("tcp", buttonGroup)
1356 radio2 = QRadioButton("http", buttonGroup)
1359 return unicode(host_line.text()).split(':')
1361 def set_button(protocol):
1363 radio1.setChecked(1)
1364 elif protocol == 'h':
1365 radio2.setChecked(1)
1367 def set_protocol(protocol):
1368 host = current_line()[0]
1370 if protocol not in pp.keys():
1371 protocol = pp.keys()[0]
1372 set_button(protocol)
1374 host_line.setText( host + ':' + port + ':' + protocol)
1376 radio1.clicked.connect(lambda x: set_protocol('t') )
1377 radio2.clicked.connect(lambda x: set_protocol('h') )
1379 set_button(current_line()[2])
1381 hbox.addWidget(QLabel(_('Protocol')+':'))
1382 hbox.addWidget(radio1)
1383 hbox.addWidget(radio2)
1385 vbox.addLayout(hbox)
1387 if wallet.interface.servers:
1388 label = _('Active Servers')
1390 label = _('Default Servers')
1392 servers_list_widget = QTreeWidget(parent)
1393 servers_list_widget.setHeaderLabels( [ label ] )
1394 servers_list_widget.setMaximumHeight(150)
1395 for host in plist.keys():
1396 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1399 host = unicode(x.text(0))
1401 if 't' in pp.keys():
1404 protocol = pp.keys()[0]
1406 host_line.setText( host + ':' + port + ':' + protocol)
1407 set_button(protocol)
1409 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1410 vbox.addWidget(servers_list_widget)
1412 vbox.addLayout(ok_cancel_buttons(d))
1415 if not d.exec_(): return
1416 server = unicode( host_line.text() )
1419 wallet.set_server(server)
1421 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1429 def closeEvent(self, event):
1430 cfg = SimpleConfig()
1432 cfg.set_key("winpos-qt", [g.left(),g.top(),g.width(),g.height()])
1439 def __init__(self, wallet, app=None):
1440 self.wallet = wallet
1442 self.app = QApplication(sys.argv)
1444 def server_list_changed(self):
1447 def waiting_dialog(self):
1453 w.setWindowTitle('Electrum')
1455 vbox = QVBoxLayout()
1460 if self.wallet.up_to_date:
1463 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1464 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1466 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1467 self.wallet.interface.poke()
1472 def restore_or_create(self):
1474 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1475 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1476 if r==2: return False
1478 is_recovery = (r==1)
1479 wallet = self.wallet
1480 # ask for the server.
1481 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1484 wallet.new_seed(None)
1485 wallet.init_mpk( wallet.seed )
1486 wallet.up_to_date_event.clear()
1487 wallet.up_to_date = False
1488 self.waiting_dialog()
1489 # run a dialog indicating the seed, ask the user to remember it
1490 ElectrumWindow.show_seed_dialog(wallet)
1492 ElectrumWindow.change_password_dialog(wallet)
1494 # ask for seed and gap.
1495 if not ElectrumWindow.seed_dialog( wallet ): return False
1496 wallet.init_mpk( wallet.seed )
1497 wallet.up_to_date_event.clear()
1498 wallet.up_to_date = False
1499 self.waiting_dialog()
1500 if wallet.is_found():
1501 # history and addressbook
1502 wallet.update_tx_history()
1503 wallet.fill_addressbook()
1504 print "Recovery successful"
1507 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1515 w = ElectrumWindow(self.wallet)
1516 if url: w.set_url(url)