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
25 print "could not import PyQt4"
26 print "on Linux systems, you may try 'sudo apt-get install python-qt4'"
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
32 import PyQt4.QtGui as QtGui
33 from interface import DEFAULT_SERVERS
38 print "Could not import icons_rc.py"
39 print "Please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'"
42 from wallet import format_satoshis
43 import bmp, mnemonic, pyqrnative
45 from decimal import Decimal
49 if platform.system() == 'Windows':
50 MONOSPACE_FONT = 'Lucida Console'
51 elif platform.system() == 'Darwin':
52 MONOSPACE_FONT = 'Monaco'
54 MONOSPACE_FONT = 'monospace'
56 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
58 def numbify(entry, is_int = False):
59 text = unicode(entry.text()).strip()
61 if not is_int: chars +='.'
62 s = ''.join([i for i in text if i in chars])
67 s = s[:p] + '.' + s[p:p+8]
69 amount = int( Decimal(s) * 100000000 )
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()
206 self.setGeometry(100,100,840,400)
207 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
208 if not self.wallet.seed: title += ' [seedless]'
209 self.setWindowTitle( title )
211 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
212 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
213 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
214 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
216 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
217 self.history_list.setFocus(True)
219 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
220 if platform.system() == 'Windows':
221 n = 3 if self.wallet.seed else 2
222 tabs.setCurrentIndex (n)
223 tabs.setCurrentIndex (0)
226 def connect_slots(self, sender):
228 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
229 self.previous_payto_e=''
231 def check_recipient(self):
232 if self.payto_e.hasFocus():
234 r = unicode( self.payto_e.text() )
235 if r != self.previous_payto_e:
236 self.previous_payto_e = r
238 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
240 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
244 s = r + ' <' + to_address + '>'
245 self.payto_e.setText(s)
248 def update_callback(self):
249 self.emit(QtCore.SIGNAL('updatesignal'))
251 def update_wallet(self):
252 if self.wallet.interface and self.wallet.interface.is_connected:
253 if self.wallet.blocks == -1:
254 text = _( "Connecting..." )
255 icon = QIcon(":icons/status_disconnected.png")
256 elif self.wallet.blocks == 0:
257 text = _( "Server not ready" )
258 icon = QIcon(":icons/status_disconnected.png")
259 elif not self.wallet.up_to_date:
260 text = _( "Synchronizing..." )
261 icon = QIcon(":icons/status_waiting.png")
263 c, u = self.wallet.get_balance()
264 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
265 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
266 icon = QIcon(":icons/status_connected.png")
268 text = _( "Not connected" )
269 icon = QIcon(":icons/status_disconnected.png")
272 text = _( "Not enough funds" )
274 self.statusBar().showMessage(text)
275 self.status_button.setIcon( icon )
277 if self.wallet.up_to_date:
278 self.textbox.setText( self.wallet.banner )
279 self.update_history_tab()
280 self.update_receive_tab()
281 self.update_contacts_tab()
282 self.update_completions()
285 def create_history_tab(self):
286 self.history_list = l = MyTreeWidget(self)
288 l.setColumnWidth(0, 40)
289 l.setColumnWidth(1, 140)
290 l.setColumnWidth(2, 350)
291 l.setColumnWidth(3, 140)
292 l.setColumnWidth(4, 140)
293 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
294 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
295 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
296 l.setContextMenuPolicy(Qt.CustomContextMenu)
297 l.customContextMenuRequested.connect(self.create_history_menu)
300 def create_history_menu(self, position):
301 self.history_list.selectedIndexes()
302 item = self.history_list.currentItem()
304 tx_hash = str(item.toolTip(0))
306 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
307 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
308 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
309 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
311 def tx_details(self, tx_hash):
312 tx = self.wallet.tx_history.get(tx_hash)
315 conf = self.wallet.blocks - tx['height'] + 1
316 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
321 tx_details = _("Transaction Details") +"\n\n" \
322 + "Transaction ID:\n" + tx_hash + "\n\n" \
323 + "Status: %d confirmations\n\n"%conf \
324 + "Date: %s\n\n"%time_str \
325 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
326 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
328 r = self.wallet.receipts.get(tx_hash)
330 tx_details += "\n_______________________________________" \
331 + '\n\nSigned URI: ' + r[2] \
332 + "\n\nSigned by: " + r[0] \
333 + '\n\nSignature: ' + r[1]
335 QMessageBox.information(self, 'Details', tx_details, 'OK')
338 def tx_label_clicked(self, item, column):
339 if column==2 and item.isSelected():
340 tx_hash = str(item.toolTip(0))
342 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
343 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
344 self.history_list.editItem( item, column )
345 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
348 def tx_label_changed(self, item, column):
352 tx_hash = str(item.toolTip(0))
353 tx = self.wallet.tx_history.get(tx_hash)
354 s = self.wallet.labels.get(tx_hash)
355 text = unicode( item.text(2) )
357 self.wallet.labels[tx_hash] = text
358 item.setForeground(2, QBrush(QColor('black')))
360 if s: self.wallet.labels.pop(tx_hash)
361 text = tx['default_label']
362 item.setText(2, text)
363 item.setForeground(2, QBrush(QColor('gray')))
366 def edit_label(self, is_recv):
367 l = self.receive_list if is_recv else self.contacts_list
368 c = 2 if is_recv else 1
369 item = l.currentItem()
370 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
371 l.editItem( item, c )
372 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
374 def address_label_clicked(self, item, column, l, column_addr, column_label):
375 if column==column_label and item.isSelected():
376 addr = unicode( item.text(column_addr) )
377 label = unicode( item.text(column_label) )
378 if label in self.wallet.aliases.keys():
380 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
381 l.editItem( item, column )
382 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
384 def address_label_changed(self, item, column, l, column_addr, column_label):
385 addr = unicode( item.text(column_addr) )
386 text = unicode( item.text(column_label) )
388 if text not in self.wallet.aliases.keys():
389 self.wallet.labels[addr] = text
391 print "error: this is one of your aliases"
392 label = self.wallet.labels.get(addr,'')
393 item.setText(column_label, QString(label))
395 s = self.wallet.labels.get(addr)
396 if s: self.wallet.labels.pop(addr)
398 self.update_history_tab()
399 self.update_completions()
401 def update_history_tab(self):
402 self.history_list.clear()
404 for tx in self.wallet.get_tx_history():
405 tx_hash = tx['tx_hash']
407 conf = self.wallet.blocks - tx['height'] + 1
408 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
409 icon = QIcon(":icons/confirmed.png")
413 icon = QIcon(":icons/unconfirmed.png")
416 label = self.wallet.labels.get(tx_hash)
417 is_default_label = (label == '') or (label is None)
418 if is_default_label: label = tx['default_label']
420 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
421 item.setFont(2, QFont(MONOSPACE_FONT))
422 item.setFont(3, QFont(MONOSPACE_FONT))
423 item.setFont(4, QFont(MONOSPACE_FONT))
424 item.setToolTip(0, tx_hash)
426 item.setForeground(2, QBrush(QColor('grey')))
428 item.setIcon(0, icon)
429 self.history_list.insertTopLevelItem(0,item)
431 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
434 def create_send_tab(self):
439 grid.setColumnMinimumWidth(3,300)
440 grid.setColumnStretch(5,1)
442 self.payto_e = QLineEdit()
443 grid.addWidget(QLabel(_('Pay to')), 1, 0)
444 grid.addWidget(self.payto_e, 1, 1, 1, 3)
445 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)
447 completer = QCompleter()
448 completer.setCaseSensitivity(False)
449 self.payto_e.setCompleter(completer)
450 completer.setModel(self.completions)
452 self.message_e = QLineEdit()
453 grid.addWidget(QLabel(_('Description')), 2, 0)
454 grid.addWidget(self.message_e, 2, 1, 1, 3)
455 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)
457 self.amount_e = QLineEdit()
458 grid.addWidget(QLabel(_('Amount')), 3, 0)
459 grid.addWidget(self.amount_e, 3, 1, 1, 2)
460 grid.addWidget(HelpButton(
461 _('Amount to be sent.') + '\n\n' \
462 + _('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)
464 self.fee_e = QLineEdit()
465 grid.addWidget(QLabel(_('Fee')), 4, 0)
466 grid.addWidget(self.fee_e, 4, 1, 1, 2)
467 grid.addWidget(HelpButton(
468 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
469 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
470 + _('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)
472 b = EnterButton(_("Send"), self.do_send)
473 grid.addWidget(b, 6, 1)
475 b = EnterButton(_("Clear"),self.do_clear)
476 grid.addWidget(b, 6, 2)
478 self.payto_sig = QLabel('')
479 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
481 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
482 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
491 def entry_changed( is_fee ):
492 self.funds_error = False
493 amount = numbify(self.amount_e)
494 fee = numbify(self.fee_e)
495 if not is_fee: fee = None
498 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
500 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
503 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
506 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
507 self.funds_error = True
508 self.amount_e.setPalette(palette)
509 self.fee_e.setPalette(palette)
511 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
512 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
517 def update_completions(self):
519 for addr,label in self.wallet.labels.items():
520 if addr in self.wallet.addressbook:
521 l.append( label + ' <' + addr + '>')
522 l = l + self.wallet.aliases.keys()
524 self.completions.setStringList(l)
530 label = unicode( self.message_e.text() )
531 r = unicode( self.payto_e.text() )
535 m1 = re.match(ALIAS_REGEXP, r)
536 # label or alias, with address in brackets
537 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
540 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
544 to_address = m2.group(2)
548 if not self.wallet.is_valid(to_address):
549 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
553 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
555 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
558 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
560 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
563 if self.wallet.use_encryption:
564 password = self.password_dialog()
571 tx = self.wallet.mktx( to_address, amount, label, password, fee)
572 except BaseException, e:
573 self.show_message(str(e))
576 status, msg = self.wallet.sendtx( tx )
578 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
580 self.update_contacts_tab()
582 QMessageBox.warning(self, _('Error'), msg, _('OK'))
585 def set_url(self, url):
586 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
587 self.tabs.setCurrentIndex(1)
588 label = self.wallet.labels.get(payto)
589 m_addr = label + ' <'+ payto+'>' if label else payto
590 self.payto_e.setText(m_addr)
592 self.message_e.setText(message)
593 self.amount_e.setText(amount)
595 self.set_frozen(self.payto_e,True)
596 self.set_frozen(self.amount_e,True)
597 self.set_frozen(self.message_e,True)
598 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
600 self.payto_sig.setVisible(False)
603 self.payto_sig.setVisible(False)
604 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
606 self.set_frozen(e,False)
608 def set_frozen(self,entry,frozen):
610 entry.setReadOnly(True)
611 entry.setFrame(False)
613 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
614 entry.setPalette(palette)
616 entry.setReadOnly(False)
619 palette.setColor(entry.backgroundRole(), QColor('white'))
620 entry.setPalette(palette)
623 def toggle_freeze(self,addr):
625 if addr in self.wallet.frozen_addresses:
626 self.wallet.unfreeze(addr)
628 self.wallet.freeze(addr)
629 self.update_receive_tab()
631 def toggle_priority(self,addr):
633 if addr in self.wallet.prioritized_addresses:
634 self.wallet.unprioritize(addr)
636 self.wallet.prioritize(addr)
637 self.update_receive_tab()
640 def create_list_tab(self, headers):
641 "generic tab creation method"
642 l = MyTreeWidget(self)
643 l.setColumnCount( len(headers) )
644 l.setHeaderLabels( headers )
654 vbox.addWidget(buttons)
659 buttons.setLayout(hbox)
664 def create_receive_tab(self):
665 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
666 l.setContextMenuPolicy(Qt.CustomContextMenu)
667 l.customContextMenuRequested.connect(self.create_receive_menu)
668 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
669 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
670 self.receive_list = l
671 self.receive_buttons_hbox = hbox
675 def create_contacts_tab(self):
676 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
677 l.setContextMenuPolicy(Qt.CustomContextMenu)
678 l.customContextMenuRequested.connect(self.create_contact_menu)
679 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
680 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
681 self.contacts_list = l
682 self.contacts_buttons_hbox = hbox
683 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
688 def create_receive_menu(self, position):
689 # fixme: this function apparently has a side effect.
690 # if it is not called the menu pops up several times
691 #self.receive_list.selectedIndexes()
693 item = self.receive_list.itemAt(position)
695 addr = unicode(item.text(1))
697 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
698 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
699 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
700 if self.wallet.expert_mode:
701 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
702 menu.addAction(t, lambda: self.toggle_freeze(addr))
703 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
704 menu.addAction(t, lambda: self.toggle_priority(addr))
705 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
708 def payto(self, x, is_alias):
715 label = self.wallet.labels.get(addr)
716 m_addr = label + ' <' + addr + '>' if label else addr
717 self.tabs.setCurrentIndex(1)
718 self.payto_e.setText(m_addr)
719 self.amount_e.setFocus()
721 def delete_contact(self, x, is_alias):
722 if self.question("Do you want to remove %s from your list of contacts?"%x):
723 if not is_alias and x in self.wallet.addressbook:
724 self.wallet.addressbook.remove(x)
725 if x in self.wallet.labels.keys():
726 self.wallet.labels.pop(x)
727 elif is_alias and x in self.wallet.aliases:
728 self.wallet.aliases.pop(x)
729 self.update_history_tab()
730 self.update_contacts_tab()
731 self.update_completions()
733 def create_contact_menu(self, position):
734 # fixme: this function apparently has a side effect.
735 # if it is not called the menu pops up several times
736 #self.contacts_list.selectedIndexes()
738 item = self.contacts_list.itemAt(position)
740 addr = unicode(item.text(0))
741 label = unicode(item.text(1))
742 is_alias = label in self.wallet.aliases.keys()
743 x = label if is_alias else addr
745 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
746 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
747 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
749 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
751 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
752 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
753 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
756 def update_receive_tab(self):
757 l = self.receive_list
759 l.setColumnHidden(0,not self.wallet.expert_mode)
760 l.setColumnHidden(3,not self.wallet.expert_mode)
761 l.setColumnHidden(4,not self.wallet.expert_mode)
762 l.setColumnWidth(0, 50)
763 l.setColumnWidth(1, 310)
764 l.setColumnWidth(2, 250)
765 l.setColumnWidth(3, 130)
766 l.setColumnWidth(4, 10)
770 for address in self.wallet.all_addresses():
772 if self.wallet.is_change(address) and not self.wallet.expert_mode:
775 label = self.wallet.labels.get(address,'')
777 h = self.wallet.history.get(address,[])
779 if not item['is_input'] : n=n+1
783 if address in self.wallet.addresses:
785 if gap > self.wallet.gap_limit:
788 if address in self.wallet.addresses:
791 c, u = self.wallet.get_addr_balance(address)
792 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
793 flags = self.wallet.get_address_flags(address)
794 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
796 item.setFont(0, QFont(MONOSPACE_FONT))
797 item.setFont(1, QFont(MONOSPACE_FONT))
798 item.setFont(3, QFont(MONOSPACE_FONT))
799 if address in self.wallet.frozen_addresses:
800 item.setBackgroundColor(1, QColor('lightblue'))
801 elif address in self.wallet.prioritized_addresses:
802 item.setBackgroundColor(1, QColor('lightgreen'))
803 if is_red and address in self.wallet.addresses:
804 item.setBackgroundColor(1, QColor('red'))
805 l.addTopLevelItem(item)
807 # we use column 1 because column 0 may be hidden
808 l.setCurrentItem(l.topLevelItem(0),1)
810 def show_contact_details(self, m):
811 a = self.wallet.aliases.get(m)
813 if a[0] in self.wallet.authorities.keys():
814 s = self.wallet.authorities.get(a[0])
817 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
818 QMessageBox.information(self, 'Alias', msg, 'OK')
820 def update_contacts_tab(self):
822 l = self.contacts_list
824 l.setColumnHidden(2, not self.wallet.expert_mode)
825 l.setColumnWidth(0, 350)
826 l.setColumnWidth(1, 330)
827 l.setColumnWidth(2, 100)
830 for alias, v in self.wallet.aliases.items():
832 alias_targets.append(target)
833 item = QTreeWidgetItem( [ target, alias, '-'] )
834 item.setBackgroundColor(0, QColor('lightgray'))
835 l.addTopLevelItem(item)
837 for address in self.wallet.addressbook:
838 if address in alias_targets: continue
839 label = self.wallet.labels.get(address,'')
841 for item in self.wallet.tx_history.values():
842 if address in item['outputs'] : n=n+1
844 item = QTreeWidgetItem( [ address, label, tx] )
845 item.setFont(0, QFont(MONOSPACE_FONT))
846 l.addTopLevelItem(item)
848 l.setCurrentItem(l.topLevelItem(0))
850 def create_wall_tab(self):
851 self.textbox = textbox = QTextEdit(self)
852 textbox.setFont(QFont(MONOSPACE_FONT))
853 textbox.setReadOnly(True)
856 def create_status_bar(self):
858 sb.setFixedHeight(35)
860 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
861 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
863 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
864 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
865 sb.addPermanentWidget( self.status_button )
866 self.setStatusBar(sb)
868 def new_contact_dialog(self):
869 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
870 address = unicode(text)
872 if self.wallet.is_valid(address):
873 self.wallet.addressbook.append(address)
875 self.update_contacts_tab()
876 self.update_history_tab()
877 self.update_completions()
879 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
882 def show_seed_dialog(wallet, parent=None):
885 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
888 if wallet.use_encryption:
889 password = parent.password_dialog()
890 if not password: return
895 seed = wallet.pw_decode( wallet.seed, password)
897 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
900 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
901 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
902 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
903 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
907 d.setWindowTitle(_("Seed"))
908 d.setMinimumSize(400, 270)
912 vbox2 = QVBoxLayout()
914 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
917 hbox.addLayout(vbox2)
918 hbox.addWidget(QLabel(msg))
930 b = QPushButton(_("Copy to Clipboard"))
931 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
933 b = QPushButton(_("View as QR Code"))
934 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
937 b = QPushButton(_("OK"))
938 b.clicked.connect(d.accept)
945 def show_seed_qrcode(seed):
949 d.setWindowTitle(_("Seed"))
950 d.setMinimumSize(270, 300)
952 vbox.addWidget(QRCodeWidget(seed))
955 b = QPushButton(_("OK"))
957 b.clicked.connect(d.accept)
964 def show_address_qrcode(self,address):
965 if not address: return
968 d.setWindowTitle(address)
969 d.setMinimumSize(270, 350)
971 qrw = QRCodeWidget(address)
975 amount_e = QLineEdit()
976 hbox.addWidget(QLabel(_('Amount')))
977 hbox.addWidget(amount_e)
980 #hbox = QHBoxLayout()
981 #label_e = QLineEdit()
982 #hbox.addWidget(QLabel('Label'))
983 #hbox.addWidget(label_e)
984 #vbox.addLayout(hbox)
986 def amount_changed():
987 amount = numbify(amount_e)
988 #label = str( label_e.getText() )
989 if amount is not None:
990 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
992 qrw.set_addr( address )
996 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
997 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
999 amount_e.textChanged.connect( amount_changed )
1001 hbox = QHBoxLayout()
1003 b = QPushButton(_("Save"))
1004 b.clicked.connect(do_save)
1006 b = QPushButton(_("Close"))
1008 b.clicked.connect(d.accept)
1010 vbox.addLayout(hbox)
1014 def question(self, msg):
1015 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1017 def show_message(self, msg):
1018 QMessageBox.information(self, _('Message'), msg, _('OK'))
1020 def password_dialog(self ):
1027 vbox = QVBoxLayout()
1028 msg = _('Please enter your password')
1029 vbox.addWidget(QLabel(msg))
1031 grid = QGridLayout()
1033 grid.addWidget(QLabel(_('Password')), 1, 0)
1034 grid.addWidget(pw, 1, 1)
1035 vbox.addLayout(grid)
1037 vbox.addLayout(ok_cancel_buttons(d))
1040 if not d.exec_(): return
1041 return unicode(pw.text())
1048 def change_password_dialog( wallet, parent=None ):
1051 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1059 new_pw = QLineEdit()
1060 new_pw.setEchoMode(2)
1061 conf_pw = QLineEdit()
1062 conf_pw.setEchoMode(2)
1064 vbox = QVBoxLayout()
1066 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')
1068 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1069 vbox.addWidget(QLabel(msg))
1071 grid = QGridLayout()
1074 if wallet.use_encryption:
1075 grid.addWidget(QLabel(_('Password')), 1, 0)
1076 grid.addWidget(pw, 1, 1)
1078 grid.addWidget(QLabel(_('New Password')), 2, 0)
1079 grid.addWidget(new_pw, 2, 1)
1081 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1082 grid.addWidget(conf_pw, 3, 1)
1083 vbox.addLayout(grid)
1085 vbox.addLayout(ok_cancel_buttons(d))
1088 if not d.exec_(): return
1090 password = unicode(pw.text()) if wallet.use_encryption else None
1091 new_password = unicode(new_pw.text())
1092 new_password2 = unicode(conf_pw.text())
1095 seed = wallet.pw_decode( wallet.seed, password)
1097 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1100 if new_password != new_password2:
1101 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1104 wallet.update_password(seed, password, new_password)
1107 def seed_dialog(wallet, parent=None):
1111 vbox = QVBoxLayout()
1112 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1113 vbox.addWidget(QLabel(msg))
1115 grid = QGridLayout()
1118 seed_e = QLineEdit()
1119 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1120 grid.addWidget(seed_e, 1, 1)
1124 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1125 grid.addWidget(gap_e, 2, 1)
1126 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1127 vbox.addLayout(grid)
1129 vbox.addLayout(ok_cancel_buttons(d))
1132 if not d.exec_(): return
1135 gap = int(unicode(gap_e.text()))
1137 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1141 seed = unicode(seed_e.text())
1144 print "not hex, trying decode"
1146 seed = mnemonic.mn_decode( seed.split(' ') )
1148 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1151 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1154 wallet.seed = str(seed)
1155 #print repr(wallet.seed)
1156 wallet.gap_limit = gap
1160 def set_expert_mode(self, b):
1161 self.wallet.expert_mode = b
1163 self.update_receive_tab()
1164 self.update_contacts_tab()
1165 # if self.wallet.seed:
1166 # self.nochange_cb.setHidden(not self.wallet.expert_mode)
1169 def settings_dialog(self):
1172 vbox = QVBoxLayout()
1173 msg = _('Here are the settings of your wallet.') + '\n'\
1174 + _('For more explanations, click on the help buttons next to each field.')
1177 label.setFixedWidth(250)
1178 label.setWordWrap(True)
1179 label.setAlignment(Qt.AlignJustify)
1180 vbox.addWidget(label)
1182 grid = QGridLayout()
1184 vbox.addLayout(grid)
1187 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1188 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1189 grid.addWidget(fee_e, 2, 1)
1190 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1191 + _('Recommended value') + ': 0.001'
1192 grid.addWidget(HelpButton(msg), 2, 2)
1193 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1196 nz_e.setText("%d"% self.wallet.num_zeros)
1197 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1198 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1199 grid.addWidget(HelpButton(msg), 3, 2)
1200 grid.addWidget(nz_e, 3, 1)
1201 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1203 cb = QCheckBox(_('Expert mode'))
1204 grid.addWidget(cb, 4, 0)
1205 cb.setChecked(self.wallet.expert_mode)
1207 if self.wallet.expert_mode:
1209 usechange_cb = QCheckBox(_('Use change addresses'))
1210 grid.addWidget(usechange_cb, 5, 0)
1211 usechange_cb.setChecked(self.wallet.use_change)
1212 grid.addWidget(HelpButton(_('Using a change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1214 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1215 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1216 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1217 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1218 + _('Warning') + ': ' \
1219 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1220 + _('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'
1222 gap_e.setText("%d"% self.wallet.gap_limit)
1223 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1224 grid.addWidget(gap_e, 6, 1)
1225 grid.addWidget(HelpButton(msg), 6, 2)
1226 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1229 vbox.addLayout(ok_cancel_buttons(d))
1233 if not d.exec_(): return
1235 fee = unicode(fee_e.text())
1237 fee = int( 100000000 * Decimal(fee) )
1239 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1242 if self.wallet.fee != fee:
1243 self.wallet.fee = fee
1246 nz = unicode(nz_e.text())
1251 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1254 if self.wallet.num_zeros != nz:
1255 self.wallet.num_zeros = nz
1256 self.update_history_tab()
1257 self.update_receive_tab()
1260 if self.wallet.expert_mode:
1262 self.wallet.use_change = usechange_cb.isChecked()
1265 n = int(gap_e.text())
1267 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1269 if self.wallet.gap_limit != n:
1270 r = self.wallet.change_gap_limit(n)
1272 self.update_receive_tab()
1274 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1276 self.set_expert_mode(cb.isChecked())
1280 def network_dialog(wallet, parent=None):
1281 interface = wallet.interface
1283 if interface.is_connected:
1284 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1286 status = _("Not connected")
1287 server = wallet.server
1290 status = _("Please choose a server.")
1291 server = random.choice( DEFAULT_SERVERS )
1293 if not wallet.interface.servers:
1295 for x in DEFAULT_SERVERS:
1296 h,port,protocol = x.split(':')
1297 servers_list.append( (h,[(protocol,port)] ) )
1299 servers_list = wallet.interface.servers
1302 for item in servers_list:
1306 protocol, port = item2
1312 d.setWindowTitle(_('Server'))
1313 d.setMinimumSize(375, 20)
1315 vbox = QVBoxLayout()
1318 hbox = QHBoxLayout()
1320 l.setPixmap(QPixmap(":icons/network.png"))
1322 hbox.addWidget(QLabel(status))
1324 vbox.addLayout(hbox)
1326 hbox = QHBoxLayout()
1327 host_line = QLineEdit()
1328 host_line.setText(server)
1329 hbox.addWidget(QLabel(_('Connect to') + ':'))
1330 hbox.addWidget(host_line)
1331 vbox.addLayout(hbox)
1333 hbox = QHBoxLayout()
1335 buttonGroup = QGroupBox(_("Protocol"))
1336 radio1 = QRadioButton("tcp", buttonGroup)
1337 radio2 = QRadioButton("http", buttonGroup)
1340 return unicode(host_line.text()).split(':')
1342 def set_button(protocol):
1344 radio1.setChecked(1)
1345 elif protocol == 'h':
1346 radio2.setChecked(1)
1348 def set_protocol(protocol):
1349 host = current_line()[0]
1351 if protocol not in pp.keys():
1352 protocol = pp.keys()[0]
1353 set_button(protocol)
1355 host_line.setText( host + ':' + port + ':' + protocol)
1357 radio1.clicked.connect(lambda x: set_protocol('t') )
1358 radio2.clicked.connect(lambda x: set_protocol('h') )
1360 set_button(current_line()[2])
1362 hbox.addWidget(QLabel(_('Protocol')+':'))
1363 hbox.addWidget(radio1)
1364 hbox.addWidget(radio2)
1366 vbox.addLayout(hbox)
1368 if wallet.interface.servers:
1369 label = _('Active Servers')
1371 label = _('Default Servers')
1373 servers_list_widget = QTreeWidget(parent)
1374 servers_list_widget.setHeaderLabels( [ label ] )
1375 servers_list_widget.setMaximumHeight(150)
1376 for host in plist.keys():
1377 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1380 host = unicode(x.text(0))
1382 if 't' in pp.keys():
1385 protocol = pp.keys()[0]
1387 host_line.setText( host + ':' + port + ':' + protocol)
1388 set_button(protocol)
1390 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1391 vbox.addWidget(servers_list_widget)
1393 vbox.addLayout(ok_cancel_buttons(d))
1396 if not d.exec_(): return
1397 server = unicode( host_line.text() )
1400 wallet.set_server(server)
1402 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1414 def __init__(self, wallet, app=None):
1415 self.wallet = wallet
1417 self.app = QApplication(sys.argv)
1419 def waiting_dialog(self):
1425 w.setWindowTitle('Electrum')
1427 vbox = QVBoxLayout()
1432 if self.wallet.up_to_date:
1435 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1436 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1438 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1439 self.wallet.interface.poke()
1444 def restore_or_create(self):
1446 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1447 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1448 if r==2: return False
1450 is_recovery = (r==1)
1451 wallet = self.wallet
1452 # ask for the server.
1453 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1456 wallet.new_seed(None)
1457 wallet.init_mpk( wallet.seed )
1458 wallet.up_to_date_event.clear()
1459 wallet.up_to_date = False
1460 self.waiting_dialog()
1461 # run a dialog indicating the seed, ask the user to remember it
1462 ElectrumWindow.show_seed_dialog(wallet)
1464 ElectrumWindow.change_password_dialog(wallet)
1466 # ask for seed and gap.
1467 if not ElectrumWindow.seed_dialog( wallet ): return False
1468 wallet.init_mpk( wallet.seed )
1469 wallet.up_to_date_event.clear()
1470 wallet.up_to_date = False
1471 self.waiting_dialog()
1472 if wallet.is_found():
1473 # history and addressbook
1474 wallet.update_tx_history()
1475 wallet.fill_addressbook()
1476 print "recovery successful"
1479 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1487 w = ElectrumWindow(self.wallet)
1488 if url: w.set_url(url)