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_rp.py"
39 print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'"
42 from wallet import format_satoshis
43 import bmp, mnemonic, pyqrnative
45 from decimal import Decimal
48 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
49 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
51 def numbify(entry, is_int = False):
52 text = unicode(entry.text()).strip()
54 if not is_int: chars +='.'
55 s = ''.join([i for i in text if i in chars])
60 s = s[:p] + '.' + s[p:p+8]
62 amount = int( Decimal(s) * 100000000 )
74 class Timer(QtCore.QThread):
77 self.emit(QtCore.SIGNAL('timersignal'))
80 class HelpButton(QPushButton):
81 def __init__(self, text):
82 QPushButton.__init__(self, '?')
83 self.setFocusPolicy(Qt.NoFocus)
84 self.setFixedWidth(20)
85 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
88 class EnterButton(QPushButton):
89 def __init__(self, text, func):
90 QPushButton.__init__(self, text)
92 self.clicked.connect(func)
94 def keyPressEvent(self, e):
95 if e.key() == QtCore.Qt.Key_Return:
98 class MyTreeWidget(QTreeWidget):
99 def __init__(self, parent):
100 QTreeWidget.__init__(self, parent)
103 for i in range(0,self.viewport().height()/5):
104 if self.itemAt(QPoint(0,i*5)) == item:
108 for j in range(0,30):
109 if self.itemAt(QPoint(0,i*5 + j)) != item:
111 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
113 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
118 class StatusBarButton(QPushButton):
119 def __init__(self, icon, tooltip, func):
120 QPushButton.__init__(self, icon, '')
121 self.setToolTip(tooltip)
123 self.setMaximumWidth(25)
124 self.clicked.connect(func)
127 def keyPressEvent(self, e):
128 if e.key() == QtCore.Qt.Key_Return:
132 class QRCodeWidget(QWidget):
134 def __init__(self, addr):
135 super(QRCodeWidget, self).__init__()
136 self.setGeometry(300, 300, 350, 350)
139 def set_addr(self, addr):
141 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
142 self.qr.addData(addr)
145 def paintEvent(self, e):
146 qp = QtGui.QPainter()
149 size = self.qr.getModuleCount()*boxsize
150 k = self.qr.getModuleCount()
151 black = QColor(0, 0, 0, 255)
152 white = QColor(255, 255, 255, 255)
155 if self.qr.isDark(r, c):
161 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
166 def ok_cancel_buttons(dialog):
169 b = QPushButton("OK")
171 b.clicked.connect(dialog.accept)
172 b = QPushButton("Cancel")
174 b.clicked.connect(dialog.reject)
178 class ElectrumWindow(QMainWindow):
180 def __init__(self, wallet):
181 QMainWindow.__init__(self)
183 self.wallet.gui_callback = self.update_callback
185 self.funds_error = False
186 self.completions = QStringListModel()
188 self.tabs = tabs = QTabWidget(self)
189 tabs.addTab(self.create_history_tab(), _('History') )
191 tabs.addTab(self.create_send_tab(), _('Send') )
192 tabs.addTab(self.create_receive_tab(), _('Receive') )
193 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
194 tabs.addTab(self.create_wall_tab(), _('Wall') )
195 tabs.setMinimumSize(600, 400)
196 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
197 self.setCentralWidget(tabs)
198 self.create_status_bar()
199 self.setGeometry(100,100,840,400)
200 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
201 if not self.wallet.seed: title += ' [seedless]'
202 self.setWindowTitle( title )
205 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
206 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
207 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
208 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
210 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
211 self.history_list.setFocus(True)
214 def connect_slots(self, sender):
216 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
217 self.previous_payto_e=''
219 def check_recipient(self):
220 if self.payto_e.hasFocus():
222 r = unicode( self.payto_e.text() )
223 if r != self.previous_payto_e:
224 self.previous_payto_e = r
226 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
228 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
232 s = r + ' <' + to_address + '>'
233 self.payto_e.setText(s)
236 def update_callback(self):
237 self.emit(QtCore.SIGNAL('updatesignal'))
239 def update_wallet(self):
240 if self.wallet.interface and self.wallet.interface.is_connected:
241 if self.wallet.blocks == -1:
242 text = _( "Connecting..." )
243 icon = QIcon(":icons/status_disconnected.png")
244 elif self.wallet.blocks == 0:
245 text = _( "Server not ready" )
246 icon = QIcon(":icons/status_disconnected.png")
247 elif not self.wallet.up_to_date:
248 text = _( "Synchronizing..." )
249 icon = QIcon(":icons/status_waiting.png")
251 c, u = self.wallet.get_balance()
252 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
253 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
254 icon = QIcon(":icons/status_connected.png")
256 text = _( "Not connected" )
257 icon = QIcon(":icons/status_disconnected.png")
260 text = _( "Not enough funds" )
262 self.statusBar().showMessage(text)
263 self.status_button.setIcon( icon )
265 if self.wallet.up_to_date:
266 self.textbox.setText( self.wallet.banner )
267 self.update_history_tab()
268 self.update_receive_tab()
269 self.update_contacts_tab()
270 self.update_completions()
273 def create_history_tab(self):
274 self.history_list = l = MyTreeWidget(self)
276 l.setColumnWidth(0, 40)
277 l.setColumnWidth(1, 140)
278 l.setColumnWidth(2, 350)
279 l.setColumnWidth(3, 140)
280 l.setColumnWidth(4, 140)
281 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
282 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
283 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
284 l.setContextMenuPolicy(Qt.CustomContextMenu)
285 l.customContextMenuRequested.connect(self.create_history_menu)
288 def create_history_menu(self, position):
289 self.history_list.selectedIndexes()
290 item = self.history_list.currentItem()
292 tx_hash = str(item.toolTip(0))
294 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
295 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
296 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
297 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
299 def tx_details(self, tx_hash):
300 tx = self.wallet.tx_history.get(tx_hash)
303 conf = self.wallet.blocks - tx['height'] + 1
304 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
309 tx_details = _("Transaction Details") +"\n\n" \
310 + "Transaction ID:\n" + tx_hash + "\n\n" \
311 + "Status: %d confirmations\n\n"%conf \
312 + "Date: %s\n\n"%time_str \
313 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
314 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
316 r = self.wallet.receipts.get(tx_hash)
318 tx_details += "\n_______________________________________" \
319 + '\n\nSigned URI: ' + r[2] \
320 + "\n\nSigned by: " + r[0] \
321 + '\n\nSignature: ' + r[1]
323 QMessageBox.information(self, 'Details', tx_details, 'OK')
326 def tx_label_clicked(self, item, column):
327 if column==2 and item.isSelected():
328 tx_hash = str(item.toolTip(0))
330 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
331 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
332 self.history_list.editItem( item, column )
333 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
336 def tx_label_changed(self, item, column):
340 tx_hash = str(item.toolTip(0))
341 tx = self.wallet.tx_history.get(tx_hash)
342 s = self.wallet.labels.get(tx_hash)
343 text = unicode( item.text(2) )
345 self.wallet.labels[tx_hash] = text
346 item.setForeground(2, QBrush(QColor('black')))
348 if s: self.wallet.labels.pop(tx_hash)
349 text = tx['default_label']
350 item.setText(2, text)
351 item.setForeground(2, QBrush(QColor('gray')))
354 def edit_label(self, is_recv):
355 l = self.receive_list if is_recv else self.contacts_list
356 c = 2 if is_recv else 1
357 item = l.currentItem()
358 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
359 l.editItem( item, c )
360 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
362 def address_label_clicked(self, item, column, l, column_addr, column_label):
363 if column==column_label and item.isSelected():
364 addr = unicode( item.text(column_addr) )
365 label = unicode( item.text(column_label) )
366 if label in self.wallet.aliases.keys():
368 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
369 l.editItem( item, column )
370 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
372 def address_label_changed(self, item, column, l, column_addr, column_label):
373 addr = unicode( item.text(column_addr) )
374 text = unicode( item.text(column_label) )
376 if text not in self.wallet.aliases.keys():
377 self.wallet.labels[addr] = text
379 print "error: this is one of your aliases"
380 label = self.wallet.labels.get(addr,'')
381 item.setText(column_label, QString(label))
383 s = self.wallet.labels.get(addr)
384 if s: self.wallet.labels.pop(addr)
386 self.update_history_tab()
387 self.update_completions()
389 def update_history_tab(self):
390 self.history_list.clear()
392 for tx in self.wallet.get_tx_history():
393 tx_hash = tx['tx_hash']
395 conf = self.wallet.blocks - tx['height'] + 1
396 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
397 icon = QIcon(":icons/confirmed.png")
401 icon = QIcon(":icons/unconfirmed.png")
404 label = self.wallet.labels.get(tx_hash)
405 is_default_label = (label == '') or (label is None)
406 if is_default_label: label = tx['default_label']
408 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
409 item.setFont(2, QFont(MONOSPACE_FONT))
410 item.setFont(3, QFont(MONOSPACE_FONT))
411 item.setFont(4, QFont(MONOSPACE_FONT))
412 item.setToolTip(0, tx_hash)
414 item.setForeground(2, QBrush(QColor('grey')))
416 item.setIcon(0, icon)
417 self.history_list.insertTopLevelItem(0,item)
419 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
422 def create_send_tab(self):
427 grid.setColumnMinimumWidth(3,300)
428 grid.setColumnStretch(5,1)
430 self.payto_e = QLineEdit()
431 grid.addWidget(QLabel(_('Pay to')), 1, 0)
432 grid.addWidget(self.payto_e, 1, 1, 1, 3)
433 grid.addWidget(HelpButton(_('Recipient of the funds.\n\nYou 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)
435 completer = QCompleter()
436 completer.setCaseSensitivity(False)
437 self.payto_e.setCompleter(completer)
438 completer.setModel(self.completions)
440 self.message_e = QLineEdit()
441 grid.addWidget(QLabel(_('Description')), 2, 0)
442 grid.addWidget(self.message_e, 2, 1, 1, 3)
443 grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).\n\nThe 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)
445 self.amount_e = QLineEdit()
446 grid.addWidget(QLabel(_('Amount')), 3, 0)
447 grid.addWidget(self.amount_e, 3, 1, 1, 2)
448 grid.addWidget(HelpButton(_('Amount to be sent.\n\nThe 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)
450 self.fee_e = QLineEdit()
451 grid.addWidget(QLabel(_('Fee')), 4, 0)
452 grid.addWidget(self.fee_e, 4, 1, 1, 2)
453 grid.addWidget(HelpButton(_('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.\n\nThe amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.\n\nA suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
455 self.nochange_cb = QCheckBox(_('Do not create change address'))
456 grid.addWidget(self.nochange_cb,5,1,1,4)
457 self.nochange_cb.setChecked(False)
458 self.nochange_cb.setHidden(not self.wallet.expert_mode)
460 b = EnterButton(_("Send"), self.do_send)
461 grid.addWidget(b, 6, 1)
463 b = EnterButton(_("Clear"),self.do_clear)
464 grid.addWidget(b, 6, 2)
466 self.payto_sig = QLabel('')
467 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
469 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
470 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
480 def entry_changed( is_fee ):
481 self.funds_error = False
482 amount = numbify(self.amount_e)
483 fee = numbify(self.fee_e)
484 if not is_fee: fee = None
487 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
489 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
492 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
495 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
496 self.funds_error = True
497 self.amount_e.setPalette(palette)
498 self.fee_e.setPalette(palette)
500 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
501 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
506 def update_completions(self):
508 for addr,label in self.wallet.labels.items():
509 if addr in self.wallet.addressbook:
510 l.append( label + ' <' + addr + '>')
511 l = l + self.wallet.aliases.keys()
513 self.completions.setStringList(l)
519 label = unicode( self.message_e.text() )
520 r = unicode( self.payto_e.text() )
524 m1 = re.match(ALIAS_REGEXP, r)
525 # label or alias, with address in brackets
526 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
529 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
533 to_address = m2.group(2)
537 if not self.wallet.is_valid(to_address):
538 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
542 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
544 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
547 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
549 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
552 if self.wallet.use_encryption:
553 password = self.password_dialog()
559 if self.nochange_cb.isChecked():
560 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
561 change_addr = inputs[0][0]
562 print "sending change to", change_addr
567 tx = self.wallet.mktx( to_address, amount, label, password, fee, change_addr )
568 except BaseException, e:
569 self.show_message(str(e))
572 status, msg = self.wallet.sendtx( tx )
574 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
576 self.update_contacts_tab()
578 QMessageBox.warning(self, _('Error'), msg, _('OK'))
581 def set_url(self, url):
582 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
583 self.tabs.setCurrentIndex(1)
584 label = self.wallet.labels.get(payto)
585 m_addr = label + ' <'+ payto+'>' if label else payto
586 self.payto_e.setText(m_addr)
588 self.message_e.setText(message)
589 self.amount_e.setText(amount)
591 self.set_frozen(self.payto_e,True)
592 self.set_frozen(self.amount_e,True)
593 self.set_frozen(self.message_e,True)
594 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
596 self.payto_sig.setVisible(False)
599 self.payto_sig.setVisible(False)
600 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
602 self.set_frozen(e,False)
604 def set_frozen(self,entry,frozen):
606 entry.setReadOnly(True)
607 entry.setFrame(False)
609 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
610 entry.setPalette(palette)
612 entry.setReadOnly(False)
615 palette.setColor(entry.backgroundRole(), QColor('white'))
616 entry.setPalette(palette)
619 def toggle_freeze(self,addr):
621 if addr in self.wallet.frozen_addresses:
622 self.wallet.unfreeze(addr)
624 self.wallet.freeze(addr)
625 self.update_receive_tab()
627 def toggle_priority(self,addr):
629 if addr in self.wallet.prioritized_addresses:
630 self.wallet.unprioritize(addr)
632 self.wallet.prioritize(addr)
633 self.update_receive_tab()
636 def create_list_tab(self, headers):
637 "generic tab creation method"
638 l = MyTreeWidget(self)
639 l.setColumnCount( len(headers) )
640 l.setHeaderLabels( headers )
650 vbox.addWidget(buttons)
655 buttons.setLayout(hbox)
660 def create_receive_tab(self):
661 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
662 l.setContextMenuPolicy(Qt.CustomContextMenu)
663 l.customContextMenuRequested.connect(self.create_receive_menu)
664 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
665 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
666 self.receive_list = l
667 self.receive_buttons_hbox = hbox
671 def create_contacts_tab(self):
672 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
673 l.setContextMenuPolicy(Qt.CustomContextMenu)
674 l.customContextMenuRequested.connect(self.create_contact_menu)
675 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
676 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
677 self.contacts_list = l
678 self.contacts_buttons_hbox = hbox
679 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
684 def create_receive_menu(self, position):
685 # fixme: this function apparently has a side effect.
686 # if it is not called the menu pops up several times
687 #self.receive_list.selectedIndexes()
689 item = self.receive_list.itemAt(position)
691 addr = unicode(item.text(1))
693 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
694 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
695 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
696 if self.wallet.expert_mode:
697 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
698 menu.addAction(t, lambda: self.toggle_freeze(addr))
699 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
700 menu.addAction(t, lambda: self.toggle_priority(addr))
701 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
704 def payto(self, x, is_alias):
711 label = self.wallet.labels.get(addr)
712 m_addr = label + ' <' + addr + '>' if label else addr
713 self.tabs.setCurrentIndex(1)
714 self.payto_e.setText(m_addr)
715 self.amount_e.setFocus()
717 def delete_contact(self, x, is_alias):
718 if self.question("Do you want to remove %s from your list of contacts?"%x):
719 if not is_alias and x in self.wallet.addressbook:
720 self.wallet.addressbook.remove(x)
721 if x in self.wallet.labels.keys():
722 self.wallet.labels.pop(x)
723 elif is_alias and x in self.wallet.aliases:
724 self.wallet.aliases.pop(x)
725 self.update_history_tab()
726 self.update_contacts_tab()
727 self.update_completions()
729 def create_contact_menu(self, position):
730 # fixme: this function apparently has a side effect.
731 # if it is not called the menu pops up several times
732 #self.contacts_list.selectedIndexes()
734 item = self.contacts_list.itemAt(position)
736 addr = unicode(item.text(0))
737 label = unicode(item.text(1))
738 is_alias = label in self.wallet.aliases.keys()
739 x = label if is_alias else addr
741 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
742 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
743 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
745 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
747 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
748 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
749 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
752 def update_receive_tab(self):
753 l = self.receive_list
755 l.setColumnHidden(0,not self.wallet.expert_mode)
756 l.setColumnHidden(3,not self.wallet.expert_mode)
757 l.setColumnHidden(4,not self.wallet.expert_mode)
758 l.setColumnWidth(0, 50)
759 l.setColumnWidth(1, 310)
760 l.setColumnWidth(2, 250)
761 l.setColumnWidth(3, 130)
762 l.setColumnWidth(4, 10)
766 for address in self.wallet.all_addresses():
768 if self.wallet.is_change(address) and not self.wallet.expert_mode:
771 label = self.wallet.labels.get(address,'')
773 h = self.wallet.history.get(address,[])
775 if not item['is_input'] : n=n+1
779 if address in self.wallet.addresses:
781 if gap > self.wallet.gap_limit:
785 if address in self.wallet.addresses:
788 c, u = self.wallet.get_addr_balance(address)
789 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
790 flags = self.wallet.get_address_flags(address)
791 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
793 item.setFont(0, QFont(MONOSPACE_FONT))
794 item.setFont(1, QFont(MONOSPACE_FONT))
795 item.setFont(3, QFont(MONOSPACE_FONT))
796 if address in self.wallet.frozen_addresses:
797 item.setBackgroundColor(1, QColor('lightblue'))
798 elif address in self.wallet.prioritized_addresses:
799 item.setBackgroundColor(1, QColor('lightgreen'))
800 if is_red and address in self.wallet.addresses:
801 item.setBackgroundColor(1, QColor('red'))
802 l.addTopLevelItem(item)
804 # we use column 1 because column 0 may be hidden
805 l.setCurrentItem(l.topLevelItem(0),1)
807 def show_contact_details(self, m):
808 a = self.wallet.aliases.get(m)
810 if a[0] in self.wallet.authorities.keys():
811 s = self.wallet.authorities.get(a[0])
814 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
815 QMessageBox.information(self, 'Alias', msg, 'OK')
817 def update_contacts_tab(self):
819 l = self.contacts_list
821 l.setColumnHidden(2, not self.wallet.expert_mode)
822 l.setColumnWidth(0, 350)
823 l.setColumnWidth(1, 330)
824 l.setColumnWidth(2, 100)
827 for alias, v in self.wallet.aliases.items():
829 alias_targets.append(target)
830 item = QTreeWidgetItem( [ target, alias, '-'] )
831 item.setBackgroundColor(0, QColor('lightgray'))
832 l.addTopLevelItem(item)
834 for address in self.wallet.addressbook:
835 if address in alias_targets: continue
836 label = self.wallet.labels.get(address,'')
838 for item in self.wallet.tx_history.values():
839 if address in item['outputs'] : n=n+1
840 tx = "None" if n==0 else "%d"%n
841 item = QTreeWidgetItem( [ address, label, tx] )
842 item.setFont(0, QFont(MONOSPACE_FONT))
843 l.addTopLevelItem(item)
845 l.setCurrentItem(l.topLevelItem(0))
847 def create_wall_tab(self):
848 self.textbox = textbox = QTextEdit(self)
849 textbox.setFont(QFont(MONOSPACE_FONT))
850 textbox.setReadOnly(True)
853 def create_status_bar(self):
855 sb.setFixedHeight(35)
857 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
858 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
860 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
861 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
862 sb.addPermanentWidget( self.status_button )
863 self.setStatusBar(sb)
865 def new_contact_dialog(self):
866 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
867 address = unicode(text)
869 if self.wallet.is_valid(address):
870 self.wallet.addressbook.append(address)
872 self.update_contacts_tab()
873 self.update_history_tab()
874 self.update_completions()
876 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
879 def show_seed_dialog(wallet, parent=None):
882 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
885 if wallet.use_encryption:
886 password = parent.password_dialog()
887 if not password: return
892 seed = wallet.pw_decode( wallet.seed, password)
894 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
897 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
898 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
899 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
900 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
904 d.setWindowTitle(_("Seed"))
905 d.setMinimumSize(400, 270)
909 vbox2 = QVBoxLayout()
911 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
914 hbox.addLayout(vbox2)
915 hbox.addWidget(QLabel(msg))
927 b = QPushButton(_("Copy to Clipboard"))
928 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
930 b = QPushButton(_("View as QR Code"))
931 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
934 b = QPushButton(_("OK"))
935 b.clicked.connect(d.accept)
942 def show_seed_qrcode(seed):
946 d.setWindowTitle(_("Seed"))
947 d.setMinimumSize(270, 300)
949 vbox.addWidget(QRCodeWidget(seed))
952 b = QPushButton(_("OK"))
954 b.clicked.connect(d.accept)
961 def show_address_qrcode(self,address):
962 if not address: return
965 d.setWindowTitle(address)
966 d.setMinimumSize(270, 350)
968 qrw = QRCodeWidget(address)
972 amount_e = QLineEdit()
973 hbox.addWidget(QLabel(_('Amount')))
974 hbox.addWidget(amount_e)
977 #hbox = QHBoxLayout()
978 #label_e = QLineEdit()
979 #hbox.addWidget(QLabel('Label'))
980 #hbox.addWidget(label_e)
981 #vbox.addLayout(hbox)
983 def amount_changed():
984 amount = numbify(amount_e)
985 #label = str( label_e.getText() )
986 if amount is not None:
987 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
989 qrw.set_addr( address )
993 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
994 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
996 amount_e.textChanged.connect( amount_changed )
1000 b = QPushButton(_("Save"))
1001 b.clicked.connect(do_save)
1003 b = QPushButton(_("Close"))
1005 b.clicked.connect(d.accept)
1007 vbox.addLayout(hbox)
1011 def question(self, msg):
1012 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1014 def show_message(self, msg):
1015 QMessageBox.information(self, _('Message'), msg, _('OK'))
1017 def password_dialog(self ):
1024 vbox = QVBoxLayout()
1025 msg = _('Please enter your password')
1026 vbox.addWidget(QLabel(msg))
1028 grid = QGridLayout()
1030 grid.addWidget(QLabel(_('Password')), 1, 0)
1031 grid.addWidget(pw, 1, 1)
1032 vbox.addLayout(grid)
1034 vbox.addLayout(ok_cancel_buttons(d))
1037 if not d.exec_(): return
1038 return unicode(pw.text())
1045 def change_password_dialog( wallet, parent=None ):
1048 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1056 new_pw = QLineEdit()
1057 new_pw.setEchoMode(2)
1058 conf_pw = QLineEdit()
1059 conf_pw.setEchoMode(2)
1061 vbox = QVBoxLayout()
1063 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')
1065 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1066 vbox.addWidget(QLabel(msg))
1068 grid = QGridLayout()
1071 if wallet.use_encryption:
1072 grid.addWidget(QLabel(_('Password')), 1, 0)
1073 grid.addWidget(pw, 1, 1)
1075 grid.addWidget(QLabel(_('New Password')), 2, 0)
1076 grid.addWidget(new_pw, 2, 1)
1078 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1079 grid.addWidget(conf_pw, 3, 1)
1080 vbox.addLayout(grid)
1082 vbox.addLayout(ok_cancel_buttons(d))
1085 if not d.exec_(): return
1087 password = unicode(pw.text()) if wallet.use_encryption else None
1088 new_password = unicode(new_pw.text())
1089 new_password2 = unicode(conf_pw.text())
1092 seed = wallet.pw_decode( wallet.seed, password)
1094 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1097 if new_password != new_password2:
1098 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1101 wallet.update_password(seed, password, new_password)
1104 def seed_dialog(wallet, parent=None):
1108 vbox = QVBoxLayout()
1109 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1110 vbox.addWidget(QLabel(msg))
1112 grid = QGridLayout()
1115 seed_e = QLineEdit()
1116 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1117 grid.addWidget(seed_e, 1, 1)
1121 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1122 grid.addWidget(gap_e, 2, 1)
1123 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1124 vbox.addLayout(grid)
1126 vbox.addLayout(ok_cancel_buttons(d))
1129 if not d.exec_(): return
1132 gap = int(unicode(gap_e.text()))
1134 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1138 seed = unicode(seed_e.text())
1141 print "not hex, trying decode"
1143 seed = mnemonic.mn_decode( seed.split(' ') )
1145 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1148 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1151 wallet.seed = str(seed)
1152 #print repr(wallet.seed)
1153 wallet.gap_limit = gap
1157 def set_expert_mode(self, b):
1158 self.wallet.expert_mode = b
1160 self.update_receive_tab()
1161 self.update_contacts_tab()
1162 if self.wallet.seed:
1163 self.nochange_cb.setHidden(not self.wallet.expert_mode)
1166 def settings_dialog(self):
1169 vbox = QVBoxLayout()
1170 msg = _('Here are the settings of your wallet.') + '\n'\
1171 + _('For more explanations, click on the help buttons next to each field.')
1174 label.setFixedWidth(250)
1175 label.setWordWrap(True)
1176 label.setAlignment(Qt.AlignJustify)
1177 vbox.addWidget(label)
1179 grid = QGridLayout()
1181 vbox.addLayout(grid)
1184 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1185 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1186 grid.addWidget(fee_e, 2, 1)
1187 grid.addWidget(HelpButton(_('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee. Recommended value: 0.001')), 2, 2)
1188 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1191 nz_e.setText("%d"% self.wallet.num_zeros)
1192 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1193 grid.addWidget(HelpButton(_('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')), 3, 2)
1194 grid.addWidget(nz_e, 3, 1)
1195 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1197 if self.wallet.expert_mode:
1198 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1199 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1200 + _('Your current gap limit is: ') + '%d'%self.wallet.gap_limit + '\n' \
1201 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1202 + _('Warning:') + ' ' \
1203 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1204 + _('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'
1206 gap_e.setText("%d"% self.wallet.gap_limit)
1207 grid.addWidget(QLabel(_('Gap limit')), 4, 0)
1208 grid.addWidget(gap_e, 4, 1)
1209 grid.addWidget(HelpButton(msg), 4, 2)
1210 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1212 cb = QCheckBox(_('Expert mode'))
1213 grid.addWidget(cb, 5, 0)
1214 cb.setChecked(self.wallet.expert_mode)
1216 vbox.addLayout(ok_cancel_buttons(d))
1220 if not d.exec_(): return
1222 fee = unicode(fee_e.text())
1224 fee = int( 100000000 * Decimal(fee) )
1226 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1229 if self.wallet.fee != fee:
1230 self.wallet.fee = fee
1233 nz = unicode(nz_e.text())
1238 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1241 if self.wallet.num_zeros != nz:
1242 self.wallet.num_zeros = nz
1243 self.update_history_tab()
1244 self.update_receive_tab()
1247 if self.wallet.expert_mode:
1249 n = int(gap_e.text())
1251 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1253 if self.wallet.gap_limit != n:
1254 r = self.wallet.change_gap_limit(n)
1256 self.update_receive_tab()
1258 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1260 self.set_expert_mode(cb.isChecked())
1264 def network_dialog(wallet, parent=None):
1265 interface = wallet.interface
1267 if interface.is_connected:
1268 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1270 status = _("Not connected")
1271 server = wallet.server
1274 status = _("Please choose a server.")
1275 server = random.choice( DEFAULT_SERVERS )
1277 if not wallet.interface.servers:
1279 for x in DEFAULT_SERVERS:
1280 h,port,protocol = x.split(':')
1281 servers_list.append( (h,[(protocol,port)] ) )
1283 servers_list = wallet.interface.servers
1286 for item in servers_list:
1290 protocol, port = item2
1296 d.setWindowTitle(_('Server'))
1297 d.setMinimumSize(375, 20)
1299 vbox = QVBoxLayout()
1302 hbox = QHBoxLayout()
1304 l.setPixmap(QPixmap(":icons/network.png"))
1306 hbox.addWidget(QLabel(status))
1308 vbox.addLayout(hbox)
1310 hbox = QHBoxLayout()
1311 host_line = QLineEdit()
1312 host_line.setText(server)
1313 hbox.addWidget(QLabel(_('Connect to') + ':'))
1314 hbox.addWidget(host_line)
1315 vbox.addLayout(hbox)
1317 hbox = QHBoxLayout()
1319 buttonGroup = QGroupBox(_("Protocol"))
1320 radio1 = QRadioButton("tcp", buttonGroup)
1321 radio2 = QRadioButton("http", buttonGroup)
1324 return unicode(host_line.text()).split(':')
1326 def set_button(protocol):
1328 radio1.setChecked(1)
1329 elif protocol == 'h':
1330 radio2.setChecked(1)
1332 def set_protocol(protocol):
1333 host = current_line()[0]
1335 if protocol not in pp.keys():
1336 protocol = pp.keys()[0]
1337 set_button(protocol)
1339 host_line.setText( host + ':' + port + ':' + protocol)
1341 radio1.clicked.connect(lambda x: set_protocol('t') )
1342 radio2.clicked.connect(lambda x: set_protocol('h') )
1344 set_button(current_line()[2])
1346 hbox.addWidget(QLabel(_('Protocol')+':'))
1347 hbox.addWidget(radio1)
1348 hbox.addWidget(radio2)
1350 vbox.addLayout(hbox)
1352 if wallet.interface.servers:
1353 label = _('Active Servers')
1355 label = _('Default Servers')
1357 servers_list_widget = QTreeWidget(parent)
1358 servers_list_widget.setHeaderLabels( [ label ] )
1359 servers_list_widget.setMaximumHeight(150)
1360 for host in plist.keys():
1361 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1364 host = unicode(x.text(0))
1366 if 't' in pp.keys():
1369 protocol = pp.keys()[0]
1371 host_line.setText( host + ':' + port + ':' + protocol)
1372 set_button(protocol)
1374 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1375 vbox.addWidget(servers_list_widget)
1377 vbox.addLayout(ok_cancel_buttons(d))
1380 if not d.exec_(): return
1381 server = unicode( host_line.text() )
1384 wallet.set_server(server)
1386 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1396 class ElectrumGui():
1398 def __init__(self, wallet):
1399 self.wallet = wallet
1400 self.app = QApplication(sys.argv)
1402 def waiting_dialog(self):
1408 w.setWindowTitle('Electrum')
1410 vbox = QVBoxLayout()
1415 if self.wallet.up_to_date:
1418 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1419 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1421 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1422 self.wallet.interface.poke()
1427 def restore_or_create(self):
1429 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1430 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1431 if r==2: return False
1433 is_recovery = (r==1)
1434 wallet = self.wallet
1435 # ask for the server.
1436 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1439 wallet.new_seed(None)
1440 wallet.init_mpk( wallet.seed )
1441 wallet.up_to_date_event.clear()
1442 wallet.up_to_date = False
1443 self.waiting_dialog()
1444 # run a dialog indicating the seed, ask the user to remember it
1445 ElectrumWindow.show_seed_dialog(wallet)
1447 ElectrumWindow.change_password_dialog(wallet)
1449 # ask for seed and gap.
1450 if not ElectrumWindow.seed_dialog( wallet ): return False
1451 wallet.init_mpk( wallet.seed )
1452 wallet.up_to_date_event.clear()
1453 wallet.up_to_date = False
1454 self.waiting_dialog()
1455 if wallet.is_found():
1456 # history and addressbook
1457 wallet.update_tx_history()
1458 wallet.fill_addressbook()
1459 print "recovery successful"
1462 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1470 w = ElectrumWindow(self.wallet)
1471 if url: w.set_url(url)