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 EnterButton(QPushButton):
81 def __init__(self, text, func):
82 QPushButton.__init__(self, text)
84 self.clicked.connect(func)
86 def keyPressEvent(self, e):
87 if e.key() == QtCore.Qt.Key_Return:
90 class MyTreeWidget(QTreeWidget):
91 def __init__(self, parent):
92 QTreeWidget.__init__(self, parent)
95 for i in range(0,100):
96 if self.itemAt(QPoint(0,i*5)) == item:
100 for j in range(0,30):
101 if self.itemAt(QPoint(0,i*5 + j)) != item:
103 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
105 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
108 def focusInEvent(self, e):
109 # we use column 1 because column 0 may be hidden
110 self.setCurrentItem(self.my_item,1)
112 def focusOutEvent(self, e):
113 self.my_item = self.currentItem()
114 self.setCurrentItem(None)
119 class StatusBarButton(QPushButton):
120 def __init__(self, icon, tooltip, func):
121 QPushButton.__init__(self, icon, '')
122 self.setToolTip(tooltip)
124 self.setMaximumWidth(25)
125 self.clicked.connect(func)
128 def keyPressEvent(self, e):
129 if e.key() == QtCore.Qt.Key_Return:
133 class QRCodeWidget(QWidget):
135 def __init__(self, addr):
136 super(QRCodeWidget, self).__init__()
137 self.setGeometry(300, 300, 350, 350)
140 def set_addr(self, addr):
142 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
143 self.qr.addData(addr)
146 def paintEvent(self, e):
147 qp = QtGui.QPainter()
150 size = self.qr.getModuleCount()*boxsize
151 k = self.qr.getModuleCount()
152 black = QColor(0, 0, 0, 255)
153 white = QColor(255, 255, 255, 255)
156 if self.qr.isDark(r, c):
162 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
167 def ok_cancel_buttons(dialog):
170 b = QPushButton("OK")
172 b.clicked.connect(dialog.accept)
173 b = QPushButton("Cancel")
175 b.clicked.connect(dialog.reject)
179 class ElectrumWindow(QMainWindow):
181 def __init__(self, wallet):
182 QMainWindow.__init__(self)
184 self.wallet.gui_callback = self.update_callback
186 self.funds_error = False
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()
291 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(4,1)
430 self.payto_e = QLineEdit()
431 grid.addWidget(QLabel(_('Pay to')), 1, 0)
432 grid.addWidget(self.payto_e, 1, 1, 1, 3)
434 completer = QCompleter()
435 self.payto_e.setCompleter(completer)
436 self.completions = QStringListModel()
437 completer.setModel(self.completions)
439 self.message_e = QLineEdit()
440 grid.addWidget(QLabel(_('Description')), 2, 0)
441 grid.addWidget(self.message_e, 2, 1, 1, 3)
443 self.amount_e = QLineEdit()
444 grid.addWidget(QLabel(_('Amount')), 3, 0)
445 grid.addWidget(self.amount_e, 3, 1, 1, 2)
447 self.nochange_cb = QCheckBox('Do not create change address')
448 grid.addWidget(self.nochange_cb,3,3)
449 self.nochange_cb.setChecked(False)
450 self.nochange_cb.setHidden(not self.wallet.expert_mode)
452 self.fee_e = QLineEdit()
453 grid.addWidget(QLabel(_('Fee')), 4, 0)
454 grid.addWidget(self.fee_e, 4, 1, 1, 2)
456 b = EnterButton(_("Send"), self.do_send)
457 grid.addWidget(b, 5, 1)
459 b = EnterButton(_("Clear"),self.do_clear)
460 grid.addWidget(b, 5, 2)
462 self.payto_sig = QLabel('')
463 grid.addWidget(self.payto_sig, 6, 0, 1, 4)
465 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
466 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
476 def entry_changed( is_fee ):
477 self.funds_error = False
478 amount = numbify(self.amount_e)
479 fee = numbify(self.fee_e)
480 if not is_fee: fee = None
483 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
485 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
488 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
491 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
492 self.funds_error = True
493 self.amount_e.setPalette(palette)
494 self.fee_e.setPalette(palette)
496 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
497 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
502 def update_completions(self):
504 for addr,label in self.wallet.labels.items():
505 if addr in self.wallet.addressbook:
506 l.append( label + ' <' + addr + '>')
507 l = l + self.wallet.aliases.keys()
509 self.completions.setStringList(l)
515 label = unicode( self.message_e.text() )
516 r = unicode( self.payto_e.text() )
520 m1 = re.match(ALIAS_REGEXP, r)
521 # label or alias, with address in brackets
522 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
525 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
529 to_address = m2.group(2)
533 if not self.wallet.is_valid(to_address):
534 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
538 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
540 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
543 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
545 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
548 if self.wallet.use_encryption:
549 password = self.password_dialog()
555 if self.nochange_cb.isChecked():
556 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
557 change_addr = inputs[0][0]
558 print "sending change to", change_addr
563 tx = self.wallet.mktx( to_address, amount, label, password, fee, change_addr )
564 except BaseException, e:
565 self.show_message(str(e))
568 status, msg = self.wallet.sendtx( tx )
570 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
572 self.update_contacts_tab()
574 QMessageBox.warning(self, _('Error'), msg, _('OK'))
577 def set_url(self, url):
578 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
579 self.tabs.setCurrentIndex(1)
580 label = self.wallet.labels.get(payto)
581 m_addr = label + ' <'+ payto+'>' if label else payto
582 self.payto_e.setText(m_addr)
584 self.message_e.setText(message)
585 self.amount_e.setText(amount)
587 self.set_frozen(self.payto_e,True)
588 self.set_frozen(self.amount_e,True)
589 self.set_frozen(self.message_e,True)
590 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
592 self.payto_sig.setVisible(False)
595 self.payto_sig.setVisible(False)
596 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
598 self.set_frozen(e,False)
600 def set_frozen(self,entry,frozen):
602 entry.setReadOnly(True)
603 entry.setFrame(False)
605 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
606 entry.setPalette(palette)
608 entry.setReadOnly(False)
611 palette.setColor(entry.backgroundRole(), QColor('white'))
612 entry.setPalette(palette)
615 def toggle_freeze(self,addr):
617 if addr in self.wallet.frozen_addresses:
618 self.wallet.unfreeze(addr)
620 self.wallet.freeze(addr)
621 self.update_receive_tab()
623 def toggle_priority(self,addr):
625 if addr in self.wallet.prioritized_addresses:
626 self.wallet.unprioritize(addr)
628 self.wallet.prioritize(addr)
629 self.update_receive_tab()
632 def create_list_tab(self, headers):
633 "generic tab creation method"
634 l = MyTreeWidget(self)
635 l.setColumnCount( len(headers) )
636 l.setHeaderLabels( headers )
646 vbox.addWidget(buttons)
651 buttons.setLayout(hbox)
656 def create_receive_tab(self):
657 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
658 l.setContextMenuPolicy(Qt.CustomContextMenu)
659 l.customContextMenuRequested.connect(self.create_receive_menu)
660 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
661 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
662 self.receive_list = l
663 self.receive_buttons_hbox = hbox
664 self.new_address_button = EnterButton(_("New"), self.change_gap_limit_dialog)
665 self.new_address_button.setHidden(not self.wallet.expert_mode)
666 hbox.addWidget(self.new_address_button)
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)
690 addr = unicode(item.text(1))
692 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
693 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
694 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
695 if self.wallet.expert_mode:
696 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
697 menu.addAction(t, lambda: self.toggle_freeze(addr))
698 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
699 menu.addAction(t, lambda: self.toggle_priority(addr))
700 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
703 def payto(self, x, is_alias):
707 s, addr = self.wallet.aliases.get(x)
710 label = self.wallet.labels.get(addr)
711 m_addr = label + ' <' + addr + '>' if label else addr
712 self.tabs.setCurrentIndex(1)
713 self.payto_e.setText(m_addr)
714 self.amount_e.setFocus()
716 def delete_contact(self, x, is_alias):
717 if self.question("Do you want to remove %s from your list of contacts?"%x):
718 if not is_alias and x in self.wallet.addressbook:
719 self.wallet.addressbook.remove(x)
720 if x in self.wallet.labels.keys():
721 self.wallet.labels.pop(x)
722 elif is_alias and x in self.wallet.aliases:
723 self.wallet.aliases.pop(x)
724 self.update_history_tab()
725 self.update_contacts_tab()
726 self.update_completions()
728 def create_contact_menu(self, position):
729 # fixme: this function apparently has a side effect.
730 # if it is not called the menu pops up several times
731 #self.contacts_list.selectedIndexes()
733 item = self.contacts_list.itemAt(position)
735 addr = unicode(item.text(0))
736 label = unicode(item.text(1))
737 is_alias = label in self.wallet.aliases.keys()
738 x = label if is_alias else addr
740 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
741 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
742 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
744 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
746 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
747 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
748 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
751 def update_receive_tab(self):
752 l = self.receive_list
754 l.setColumnHidden(0,not self.wallet.expert_mode)
755 l.setColumnHidden(3,not self.wallet.expert_mode)
756 l.setColumnHidden(4,not self.wallet.expert_mode)
757 l.setColumnWidth(0, 50)
758 l.setColumnWidth(1, 310)
759 l.setColumnWidth(2, 250)
760 l.setColumnWidth(3, 130)
761 l.setColumnWidth(4, 10)
763 self.new_address_button.setHidden(not self.wallet.expert_mode)
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(1, 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())
1041 def change_gap_limit_dialog(self):
1045 vbox = QVBoxLayout()
1047 msg = _('In order to create more addresses, you need to raise your gap limit.') + '\n' \
1048 + _('The gap limit is the maximal number of contiguous unused addresses in your wallet.') + '\n\n' \
1049 + _('Warning:') + '\n' \
1050 + _('This parameter must be provided in order to recover your wallet from seed.') + '\n' \
1051 + _('Do not modify it if you do not understand what you are doing!!!') + '\n\n' \
1052 + _('Your current gap limit is: ') + '%d'%self.wallet.gap_limit + '\n' \
1053 + _('The minimum for this wallet is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n'
1055 vbox.addWidget(QLabel(msg))
1057 grid = QGridLayout()
1059 grid.addWidget(QLabel(_('New gap limit: ')), 1, 0)
1062 grid.addWidget(e, 1, 1)
1063 vbox.addLayout(grid)
1065 vbox.addLayout(ok_cancel_buttons(d))
1069 if not d.exec_(): return
1073 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1076 r = self.wallet.change_gap_limit(n)
1078 self.update_receive_tab()
1080 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1087 def change_password_dialog( wallet, parent=None ):
1090 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1098 new_pw = QLineEdit()
1099 new_pw.setEchoMode(2)
1100 conf_pw = QLineEdit()
1101 conf_pw.setEchoMode(2)
1103 vbox = QVBoxLayout()
1105 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')
1107 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1108 vbox.addWidget(QLabel(msg))
1110 grid = QGridLayout()
1113 if wallet.use_encryption:
1114 grid.addWidget(QLabel(_('Password')), 1, 0)
1115 grid.addWidget(pw, 1, 1)
1117 grid.addWidget(QLabel(_('New Password')), 2, 0)
1118 grid.addWidget(new_pw, 2, 1)
1120 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1121 grid.addWidget(conf_pw, 3, 1)
1122 vbox.addLayout(grid)
1124 vbox.addLayout(ok_cancel_buttons(d))
1127 if not d.exec_(): return
1129 password = unicode(pw.text()) if wallet.use_encryption else None
1130 new_password = unicode(new_pw.text())
1131 new_password2 = unicode(conf_pw.text())
1134 seed = wallet.pw_decode( wallet.seed, password)
1136 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1139 if new_password != new_password2:
1140 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1143 wallet.update_password(seed, password, new_password)
1146 def seed_dialog(wallet, parent=None):
1150 vbox = QVBoxLayout()
1151 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1152 vbox.addWidget(QLabel(msg))
1154 grid = QGridLayout()
1157 seed_e = QLineEdit()
1158 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1159 grid.addWidget(seed_e, 1, 1)
1163 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1164 grid.addWidget(gap_e, 2, 1)
1165 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1166 vbox.addLayout(grid)
1168 vbox.addLayout(ok_cancel_buttons(d))
1171 if not d.exec_(): return
1174 gap = int(unicode(gap_e.text()))
1176 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1180 seed = unicode(seed_e.text())
1183 print "not hex, trying decode"
1185 seed = mnemonic.mn_decode( seed.split(' ') )
1187 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1190 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1193 wallet.seed = str(seed)
1194 #print repr(wallet.seed)
1195 wallet.gap_limit = gap
1199 def set_expert_mode(self, b):
1200 self.wallet.expert_mode = b
1202 self.update_receive_tab()
1203 self.update_contacts_tab()
1204 self.nochange_cb.setHidden(not self.wallet.expert_mode)
1208 def settings_dialog(self):
1212 vbox = QVBoxLayout()
1214 msg = _('Here are the settings of your wallet.')
1215 vbox.addWidget(QLabel(msg))
1217 grid = QGridLayout()
1219 vbox.addLayout(grid)
1222 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1223 grid.addWidget(QLabel(_('Fee per tx. input')), 2, 0)
1224 grid.addWidget(fee_e, 2, 1)
1225 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1228 nz_e.setText("%d"% self.wallet.num_zeros)
1229 grid.addWidget(QLabel(_('Zeros displayed after decimal point')), 3, 0)
1230 grid.addWidget(nz_e, 3, 1)
1231 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1233 cb = QCheckBox('Expert mode')
1234 grid.addWidget(cb,4,0)
1235 cb.setChecked(self.wallet.expert_mode)
1237 vbox.addLayout(ok_cancel_buttons(d))
1240 if not d.exec_(): return
1242 self.set_expert_mode(cb.isChecked())
1244 fee = unicode(fee_e.text())
1246 fee = int( 100000000 * Decimal(fee) )
1248 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1251 if self.wallet.fee != fee:
1252 self.wallet.fee = fee
1255 nz = unicode(nz_e.text())
1260 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1263 if self.wallet.num_zeros != nz:
1264 self.wallet.num_zeros = nz
1265 self.update_history_tab()
1266 self.update_receive_tab()
1270 def network_dialog(wallet, parent=None):
1271 interface = wallet.interface
1273 if interface.is_connected:
1274 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1276 status = _("Not connected")
1277 server = wallet.server
1280 status = _("Please choose a server.")
1281 server = random.choice( DEFAULT_SERVERS )
1283 if not wallet.interface.servers:
1285 for x in DEFAULT_SERVERS:
1286 h,port,protocol = x.split(':')
1287 servers_list.append( (h,[(protocol,port)] ) )
1289 servers_list = wallet.interface.servers
1292 for item in servers_list:
1296 protocol, port = item2
1302 d.setWindowTitle(_('Server'))
1303 d.setMinimumSize(375, 20)
1305 vbox = QVBoxLayout()
1308 hbox = QHBoxLayout()
1310 l.setPixmap(QPixmap(":icons/network.png"))
1312 hbox.addWidget(QLabel(status))
1314 vbox.addLayout(hbox)
1316 hbox = QHBoxLayout()
1317 host_line = QLineEdit()
1318 host_line.setText(server)
1319 hbox.addWidget(QLabel(_('Connect to') + ':'))
1320 hbox.addWidget(host_line)
1321 vbox.addLayout(hbox)
1323 hbox = QHBoxLayout()
1325 buttonGroup = QGroupBox(_("Protocol"))
1326 radio1 = QRadioButton("tcp", buttonGroup)
1327 radio2 = QRadioButton("http", buttonGroup)
1330 return unicode(host_line.text()).split(':')
1332 def set_button(protocol):
1334 radio1.setChecked(1)
1335 elif protocol == 'h':
1336 radio2.setChecked(1)
1338 def set_protocol(protocol):
1339 host = current_line()[0]
1341 if protocol not in pp.keys():
1342 protocol = pp.keys()[0]
1343 set_button(protocol)
1345 host_line.setText( host + ':' + port + ':' + protocol)
1347 radio1.clicked.connect(lambda x: set_protocol('t') )
1348 radio2.clicked.connect(lambda x: set_protocol('h') )
1350 set_button(current_line()[2])
1352 hbox.addWidget(QLabel(_('Protocol')+':'))
1353 hbox.addWidget(radio1)
1354 hbox.addWidget(radio2)
1356 vbox.addLayout(hbox)
1358 if wallet.interface.servers:
1359 label = _('Active Servers')
1361 label = _('Default Servers')
1363 servers_list_widget = QTreeWidget(parent)
1364 servers_list_widget.setHeaderLabels( [ label ] )
1365 servers_list_widget.setMaximumHeight(150)
1366 for host in plist.keys():
1367 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1370 host = unicode(x.text(0))
1372 if 't' in pp.keys():
1375 protocol = pp.keys()[0]
1377 host_line.setText( host + ':' + port + ':' + protocol)
1378 set_button(protocol)
1380 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1381 vbox.addWidget(servers_list_widget)
1383 vbox.addLayout(ok_cancel_buttons(d))
1386 if not d.exec_(): return
1387 server = unicode( host_line.text() )
1390 wallet.set_server(server)
1392 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1402 class ElectrumGui():
1404 def __init__(self, wallet):
1405 self.wallet = wallet
1406 self.app = QApplication(sys.argv)
1408 def waiting_dialog(self):
1414 w.setWindowTitle('Electrum')
1416 vbox = QVBoxLayout()
1421 if self.wallet.up_to_date:
1424 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1425 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1427 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1428 self.wallet.interface.poke()
1433 def restore_or_create(self):
1435 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1436 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1437 if r==2: return False
1439 is_recovery = (r==1)
1440 wallet = self.wallet
1441 # ask for the server.
1442 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1445 wallet.new_seed(None)
1446 wallet.init_mpk( wallet.seed )
1447 wallet.up_to_date_event.clear()
1448 wallet.up_to_date = False
1449 self.waiting_dialog()
1450 # run a dialog indicating the seed, ask the user to remember it
1451 ElectrumWindow.show_seed_dialog(wallet)
1453 ElectrumWindow.change_password_dialog(wallet)
1455 # ask for seed and gap.
1456 if not ElectrumWindow.seed_dialog( wallet ): return False
1457 wallet.init_mpk( wallet.seed )
1458 wallet.up_to_date_event.clear()
1459 wallet.up_to_date = False
1460 self.waiting_dialog()
1461 if wallet.is_found():
1462 # history and addressbook
1463 wallet.update_tx_history()
1464 wallet.fill_addressbook()
1465 print "recovery successful"
1468 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1476 w = ElectrumWindow(self.wallet)
1477 if url: w.set_url(url)