3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re
21 from util import print_error
26 print_error("Error: Could not import PyQt4")
27 print_error("on Linux systems, you may try 'sudo apt-get install python-qt4'")
30 from PyQt4.QtGui import *
31 from PyQt4.QtCore import *
32 import PyQt4.QtCore as QtCore
33 import PyQt4.QtGui as QtGui
34 from interface import DEFAULT_SERVERS
39 print_error("Error: Could not import icons_rc.py")
40 print_error("Please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'")
43 from wallet import format_satoshis
44 import bmp, mnemonic, pyqrnative
46 from decimal import Decimal
50 if platform.system() == 'Windows':
51 MONOSPACE_FONT = 'Lucida Console'
52 elif platform.system() == 'Darwin':
53 MONOSPACE_FONT = 'Monaco'
55 MONOSPACE_FONT = 'monospace'
57 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
59 def numbify(entry, is_int = False):
60 text = unicode(entry.text()).strip()
62 if not is_int: chars +='.'
63 s = ''.join([i for i in text if i in chars])
68 s = s[:p] + '.' + s[p:p+8]
70 amount = int( Decimal(s) * 100000000 )
82 class Timer(QtCore.QThread):
85 self.emit(QtCore.SIGNAL('timersignal'))
88 class HelpButton(QPushButton):
89 def __init__(self, text):
90 QPushButton.__init__(self, '?')
91 self.setFocusPolicy(Qt.NoFocus)
92 self.setFixedWidth(20)
93 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
96 class EnterButton(QPushButton):
97 def __init__(self, text, func):
98 QPushButton.__init__(self, text)
100 self.clicked.connect(func)
102 def keyPressEvent(self, e):
103 if e.key() == QtCore.Qt.Key_Return:
106 class MyTreeWidget(QTreeWidget):
107 def __init__(self, parent):
108 QTreeWidget.__init__(self, parent)
111 for i in range(0,self.viewport().height()/5):
112 if self.itemAt(QPoint(0,i*5)) == item:
116 for j in range(0,30):
117 if self.itemAt(QPoint(0,i*5 + j)) != item:
119 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
121 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
126 class StatusBarButton(QPushButton):
127 def __init__(self, icon, tooltip, func):
128 QPushButton.__init__(self, icon, '')
129 self.setToolTip(tooltip)
131 self.setMaximumWidth(25)
132 self.clicked.connect(func)
135 def keyPressEvent(self, e):
136 if e.key() == QtCore.Qt.Key_Return:
140 class QRCodeWidget(QWidget):
142 def __init__(self, addr):
143 super(QRCodeWidget, self).__init__()
144 self.setGeometry(300, 300, 350, 350)
147 def set_addr(self, addr):
149 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
150 self.qr.addData(addr)
153 def paintEvent(self, e):
154 qp = QtGui.QPainter()
157 size = self.qr.getModuleCount()*boxsize
158 k = self.qr.getModuleCount()
159 black = QColor(0, 0, 0, 255)
160 white = QColor(255, 255, 255, 255)
163 if self.qr.isDark(r, c):
169 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
174 def ok_cancel_buttons(dialog):
177 b = QPushButton("OK")
179 b.clicked.connect(dialog.accept)
180 b = QPushButton("Cancel")
182 b.clicked.connect(dialog.reject)
186 class ElectrumWindow(QMainWindow):
188 def __init__(self, wallet):
189 QMainWindow.__init__(self)
191 self.wallet.register_callback(self.update_callback)
193 self.funds_error = False
194 self.completions = QStringListModel()
196 self.tabs = tabs = QTabWidget(self)
197 tabs.addTab(self.create_history_tab(), _('History') )
199 tabs.addTab(self.create_send_tab(), _('Send') )
200 tabs.addTab(self.create_receive_tab(), _('Receive') )
201 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
202 tabs.addTab(self.create_wall_tab(), _('Wall') )
203 tabs.setMinimumSize(600, 400)
204 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
205 self.setCentralWidget(tabs)
206 self.create_status_bar()
207 self.setGeometry(100,100,840,400)
208 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
209 if not self.wallet.seed: title += ' [seedless]'
210 self.setWindowTitle( title )
212 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
213 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
214 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
215 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
217 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
218 self.history_list.setFocus(True)
220 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
221 if platform.system() == 'Windows':
222 n = 3 if self.wallet.seed else 2
223 tabs.setCurrentIndex (n)
224 tabs.setCurrentIndex (0)
227 def connect_slots(self, sender):
229 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
230 self.previous_payto_e=''
232 def check_recipient(self):
233 if self.payto_e.hasFocus():
235 r = unicode( self.payto_e.text() )
236 if r != self.previous_payto_e:
237 self.previous_payto_e = r
239 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
241 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
245 s = r + ' <' + to_address + '>'
246 self.payto_e.setText(s)
249 def update_callback(self):
250 self.emit(QtCore.SIGNAL('updatesignal'))
252 def update_wallet(self):
253 if self.wallet.interface and self.wallet.interface.is_connected:
254 if self.wallet.blocks == -1:
255 text = _( "Connecting..." )
256 icon = QIcon(":icons/status_disconnected.png")
257 elif self.wallet.blocks == 0:
258 text = _( "Server not ready" )
259 icon = QIcon(":icons/status_disconnected.png")
260 elif not self.wallet.up_to_date:
261 text = _( "Synchronizing..." )
262 icon = QIcon(":icons/status_waiting.png")
264 c, u = self.wallet.get_balance()
265 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
266 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
267 icon = QIcon(":icons/status_connected.png")
269 text = _( "Not connected" )
270 icon = QIcon(":icons/status_disconnected.png")
273 text = _( "Not enough funds" )
275 self.statusBar().showMessage(text)
276 self.status_button.setIcon( icon )
278 if self.wallet.up_to_date:
279 self.textbox.setText( self.wallet.banner )
280 self.update_history_tab()
281 self.update_receive_tab()
282 self.update_contacts_tab()
283 self.update_completions()
286 def create_history_tab(self):
287 self.history_list = l = MyTreeWidget(self)
289 l.setColumnWidth(0, 40)
290 l.setColumnWidth(1, 140)
291 l.setColumnWidth(2, 350)
292 l.setColumnWidth(3, 140)
293 l.setColumnWidth(4, 140)
294 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
295 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
296 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
297 l.setContextMenuPolicy(Qt.CustomContextMenu)
298 l.customContextMenuRequested.connect(self.create_history_menu)
301 def create_history_menu(self, position):
302 self.history_list.selectedIndexes()
303 item = self.history_list.currentItem()
305 tx_hash = str(item.toolTip(0))
307 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
308 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
309 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
310 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
312 def tx_details(self, tx_hash):
313 tx = self.wallet.tx_history.get(tx_hash)
316 conf = self.wallet.blocks - tx['height'] + 1
317 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
322 tx_details = _("Transaction Details") +"\n\n" \
323 + "Transaction ID:\n" + tx_hash + "\n\n" \
324 + "Status: %d confirmations\n\n"%conf \
325 + "Date: %s\n\n"%time_str \
326 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
327 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
329 r = self.wallet.receipts.get(tx_hash)
331 tx_details += "\n_______________________________________" \
332 + '\n\nSigned URI: ' + r[2] \
333 + "\n\nSigned by: " + r[0] \
334 + '\n\nSignature: ' + r[1]
336 QMessageBox.information(self, 'Details', tx_details, 'OK')
339 def tx_label_clicked(self, item, column):
340 if column==2 and item.isSelected():
341 tx_hash = str(item.toolTip(0))
343 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
344 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
345 self.history_list.editItem( item, column )
346 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
349 def tx_label_changed(self, item, column):
353 tx_hash = str(item.toolTip(0))
354 tx = self.wallet.tx_history.get(tx_hash)
355 s = self.wallet.labels.get(tx_hash)
356 text = unicode( item.text(2) )
358 self.wallet.labels[tx_hash] = text
359 item.setForeground(2, QBrush(QColor('black')))
361 if s: self.wallet.labels.pop(tx_hash)
362 text = tx['default_label']
363 item.setText(2, text)
364 item.setForeground(2, QBrush(QColor('gray')))
367 def edit_label(self, is_recv):
368 l = self.receive_list if is_recv else self.contacts_list
369 c = 2 if is_recv else 1
370 item = l.currentItem()
371 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
372 l.editItem( item, c )
373 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
375 def address_label_clicked(self, item, column, l, column_addr, column_label):
376 if column==column_label and item.isSelected():
377 addr = unicode( item.text(column_addr) )
378 label = unicode( item.text(column_label) )
379 if label in self.wallet.aliases.keys():
381 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
382 l.editItem( item, column )
383 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
385 def address_label_changed(self, item, column, l, column_addr, column_label):
386 addr = unicode( item.text(column_addr) )
387 text = unicode( item.text(column_label) )
389 if text not in self.wallet.aliases.keys():
390 self.wallet.labels[addr] = text
392 print_error("Error: This is one of your aliases")
393 label = self.wallet.labels.get(addr,'')
394 item.setText(column_label, QString(label))
396 s = self.wallet.labels.get(addr)
397 if s: self.wallet.labels.pop(addr)
399 self.update_history_tab()
400 self.update_completions()
402 def update_history_tab(self):
403 self.history_list.clear()
405 for tx in self.wallet.get_tx_history():
406 tx_hash = tx['tx_hash']
408 conf = self.wallet.blocks - tx['height'] + 1
409 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
410 icon = QIcon(":icons/confirmed.png")
414 icon = QIcon(":icons/unconfirmed.png")
417 label = self.wallet.labels.get(tx_hash)
418 is_default_label = (label == '') or (label is None)
419 if is_default_label: label = tx['default_label']
421 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
422 item.setFont(2, QFont(MONOSPACE_FONT))
423 item.setFont(3, QFont(MONOSPACE_FONT))
424 item.setFont(4, QFont(MONOSPACE_FONT))
425 item.setToolTip(0, tx_hash)
427 item.setForeground(2, QBrush(QColor('grey')))
429 item.setIcon(0, icon)
430 self.history_list.insertTopLevelItem(0,item)
432 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
435 def create_send_tab(self):
440 grid.setColumnMinimumWidth(3,300)
441 grid.setColumnStretch(5,1)
443 self.payto_e = QLineEdit()
444 grid.addWidget(QLabel(_('Pay to')), 1, 0)
445 grid.addWidget(self.payto_e, 1, 1, 1, 3)
446 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)
448 completer = QCompleter()
449 completer.setCaseSensitivity(False)
450 self.payto_e.setCompleter(completer)
451 completer.setModel(self.completions)
453 self.message_e = QLineEdit()
454 grid.addWidget(QLabel(_('Description')), 2, 0)
455 grid.addWidget(self.message_e, 2, 1, 1, 3)
456 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)
458 self.amount_e = QLineEdit()
459 grid.addWidget(QLabel(_('Amount')), 3, 0)
460 grid.addWidget(self.amount_e, 3, 1, 1, 2)
461 grid.addWidget(HelpButton(
462 _('Amount to be sent.') + '\n\n' \
463 + _('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)
465 self.fee_e = QLineEdit()
466 grid.addWidget(QLabel(_('Fee')), 4, 0)
467 grid.addWidget(self.fee_e, 4, 1, 1, 2)
468 grid.addWidget(HelpButton(
469 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
470 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
471 + _('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)
473 b = EnterButton(_("Send"), self.do_send)
474 grid.addWidget(b, 6, 1)
476 b = EnterButton(_("Clear"),self.do_clear)
477 grid.addWidget(b, 6, 2)
479 self.payto_sig = QLabel('')
480 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
482 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
483 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
492 def entry_changed( is_fee ):
493 self.funds_error = False
494 amount = numbify(self.amount_e)
495 fee = numbify(self.fee_e)
496 if not is_fee: fee = None
499 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
501 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
504 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
507 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
508 self.funds_error = True
509 self.amount_e.setPalette(palette)
510 self.fee_e.setPalette(palette)
512 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
513 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
518 def update_completions(self):
520 for addr,label in self.wallet.labels.items():
521 if addr in self.wallet.addressbook:
522 l.append( label + ' <' + addr + '>')
523 l = l + self.wallet.aliases.keys()
525 self.completions.setStringList(l)
531 label = unicode( self.message_e.text() )
532 r = unicode( self.payto_e.text() )
536 m1 = re.match(ALIAS_REGEXP, r)
537 # label or alias, with address in brackets
538 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
541 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
545 to_address = m2.group(2)
549 if not self.wallet.is_valid(to_address):
550 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
554 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
556 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
559 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
561 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
564 if self.wallet.use_encryption:
565 password = self.password_dialog()
572 tx = self.wallet.mktx( to_address, amount, label, password, fee)
573 except BaseException, e:
574 self.show_message(str(e))
577 status, msg = self.wallet.sendtx( tx )
579 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
581 self.update_contacts_tab()
583 QMessageBox.warning(self, _('Error'), msg, _('OK'))
586 def set_url(self, url):
587 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
588 self.tabs.setCurrentIndex(1)
589 label = self.wallet.labels.get(payto)
590 m_addr = label + ' <'+ payto+'>' if label else payto
591 self.payto_e.setText(m_addr)
593 self.message_e.setText(message)
594 self.amount_e.setText(amount)
596 self.set_frozen(self.payto_e,True)
597 self.set_frozen(self.amount_e,True)
598 self.set_frozen(self.message_e,True)
599 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
601 self.payto_sig.setVisible(False)
604 self.payto_sig.setVisible(False)
605 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
607 self.set_frozen(e,False)
609 def set_frozen(self,entry,frozen):
611 entry.setReadOnly(True)
612 entry.setFrame(False)
614 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
615 entry.setPalette(palette)
617 entry.setReadOnly(False)
620 palette.setColor(entry.backgroundRole(), QColor('white'))
621 entry.setPalette(palette)
624 def toggle_freeze(self,addr):
626 if addr in self.wallet.frozen_addresses:
627 self.wallet.unfreeze(addr)
629 self.wallet.freeze(addr)
630 self.update_receive_tab()
632 def toggle_priority(self,addr):
634 if addr in self.wallet.prioritized_addresses:
635 self.wallet.unprioritize(addr)
637 self.wallet.prioritize(addr)
638 self.update_receive_tab()
641 def create_list_tab(self, headers):
642 "generic tab creation method"
643 l = MyTreeWidget(self)
644 l.setColumnCount( len(headers) )
645 l.setHeaderLabels( headers )
655 vbox.addWidget(buttons)
660 buttons.setLayout(hbox)
665 def create_receive_tab(self):
666 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
667 l.setContextMenuPolicy(Qt.CustomContextMenu)
668 l.customContextMenuRequested.connect(self.create_receive_menu)
669 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
670 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
671 self.receive_list = l
672 self.receive_buttons_hbox = hbox
676 def create_contacts_tab(self):
677 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
678 l.setContextMenuPolicy(Qt.CustomContextMenu)
679 l.customContextMenuRequested.connect(self.create_contact_menu)
680 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
681 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
682 self.contacts_list = l
683 self.contacts_buttons_hbox = hbox
684 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
689 def create_receive_menu(self, position):
690 # fixme: this function apparently has a side effect.
691 # if it is not called the menu pops up several times
692 #self.receive_list.selectedIndexes()
694 item = self.receive_list.itemAt(position)
696 addr = unicode(item.text(1))
698 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
699 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
700 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
701 if self.wallet.expert_mode:
702 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
703 menu.addAction(t, lambda: self.toggle_freeze(addr))
704 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
705 menu.addAction(t, lambda: self.toggle_priority(addr))
706 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
709 def payto(self, x, is_alias):
716 label = self.wallet.labels.get(addr)
717 m_addr = label + ' <' + addr + '>' if label else addr
718 self.tabs.setCurrentIndex(1)
719 self.payto_e.setText(m_addr)
720 self.amount_e.setFocus()
722 def delete_contact(self, x, is_alias):
723 if self.question("Do you want to remove %s from your list of contacts?"%x):
724 if not is_alias and x in self.wallet.addressbook:
725 self.wallet.addressbook.remove(x)
726 if x in self.wallet.labels.keys():
727 self.wallet.labels.pop(x)
728 elif is_alias and x in self.wallet.aliases:
729 self.wallet.aliases.pop(x)
730 self.update_history_tab()
731 self.update_contacts_tab()
732 self.update_completions()
734 def create_contact_menu(self, position):
735 # fixme: this function apparently has a side effect.
736 # if it is not called the menu pops up several times
737 #self.contacts_list.selectedIndexes()
739 item = self.contacts_list.itemAt(position)
741 addr = unicode(item.text(0))
742 label = unicode(item.text(1))
743 is_alias = label in self.wallet.aliases.keys()
744 x = label if is_alias else addr
746 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
747 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
748 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
750 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
752 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
753 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
754 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
757 def update_receive_tab(self):
758 l = self.receive_list
760 l.setColumnHidden(0,not self.wallet.expert_mode)
761 l.setColumnHidden(3,not self.wallet.expert_mode)
762 l.setColumnHidden(4,not self.wallet.expert_mode)
763 l.setColumnWidth(0, 50)
764 l.setColumnWidth(1, 310)
765 l.setColumnWidth(2, 250)
766 l.setColumnWidth(3, 130)
767 l.setColumnWidth(4, 10)
771 for address in self.wallet.all_addresses():
773 if self.wallet.is_change(address) and not self.wallet.expert_mode:
776 label = self.wallet.labels.get(address,'')
778 h = self.wallet.history.get(address,[])
780 if not item['is_input'] : n=n+1
784 if address in self.wallet.addresses:
786 if gap > self.wallet.gap_limit:
789 if address in self.wallet.addresses:
792 c, u = self.wallet.get_addr_balance(address)
793 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
794 flags = self.wallet.get_address_flags(address)
795 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
797 item.setFont(0, QFont(MONOSPACE_FONT))
798 item.setFont(1, QFont(MONOSPACE_FONT))
799 item.setFont(3, QFont(MONOSPACE_FONT))
800 if address in self.wallet.frozen_addresses:
801 item.setBackgroundColor(1, QColor('lightblue'))
802 elif address in self.wallet.prioritized_addresses:
803 item.setBackgroundColor(1, QColor('lightgreen'))
804 if is_red and address in self.wallet.addresses:
805 item.setBackgroundColor(1, QColor('red'))
806 l.addTopLevelItem(item)
808 # we use column 1 because column 0 may be hidden
809 l.setCurrentItem(l.topLevelItem(0),1)
811 def show_contact_details(self, m):
812 a = self.wallet.aliases.get(m)
814 if a[0] in self.wallet.authorities.keys():
815 s = self.wallet.authorities.get(a[0])
818 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
819 QMessageBox.information(self, 'Alias', msg, 'OK')
821 def update_contacts_tab(self):
823 l = self.contacts_list
825 l.setColumnHidden(2, not self.wallet.expert_mode)
826 l.setColumnWidth(0, 350)
827 l.setColumnWidth(1, 330)
828 l.setColumnWidth(2, 100)
831 for alias, v in self.wallet.aliases.items():
833 alias_targets.append(target)
834 item = QTreeWidgetItem( [ target, alias, '-'] )
835 item.setBackgroundColor(0, QColor('lightgray'))
836 l.addTopLevelItem(item)
838 for address in self.wallet.addressbook:
839 if address in alias_targets: continue
840 label = self.wallet.labels.get(address,'')
842 for item in self.wallet.tx_history.values():
843 if address in item['outputs'] : n=n+1
845 item = QTreeWidgetItem( [ address, label, tx] )
846 item.setFont(0, QFont(MONOSPACE_FONT))
847 l.addTopLevelItem(item)
849 l.setCurrentItem(l.topLevelItem(0))
851 def create_wall_tab(self):
852 self.textbox = textbox = QTextEdit(self)
853 textbox.setFont(QFont(MONOSPACE_FONT))
854 textbox.setReadOnly(True)
857 def create_status_bar(self):
859 sb.setFixedHeight(35)
861 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
862 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
864 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
865 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
866 sb.addPermanentWidget( self.status_button )
867 self.setStatusBar(sb)
869 def new_contact_dialog(self):
870 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
871 address = unicode(text)
873 if self.wallet.is_valid(address):
874 self.wallet.addressbook.append(address)
876 self.update_contacts_tab()
877 self.update_history_tab()
878 self.update_completions()
880 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
883 def show_seed_dialog(wallet, parent=None):
886 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
889 if wallet.use_encryption:
890 password = parent.password_dialog()
891 if not password: return
896 seed = wallet.pw_decode( wallet.seed, password)
898 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
901 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
902 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
903 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
904 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
908 d.setWindowTitle(_("Seed"))
909 d.setMinimumSize(400, 270)
913 vbox2 = QVBoxLayout()
915 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
918 hbox.addLayout(vbox2)
919 hbox.addWidget(QLabel(msg))
931 b = QPushButton(_("Copy to Clipboard"))
932 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
934 b = QPushButton(_("View as QR Code"))
935 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
938 b = QPushButton(_("OK"))
939 b.clicked.connect(d.accept)
946 def show_seed_qrcode(seed):
950 d.setWindowTitle(_("Seed"))
951 d.setMinimumSize(270, 300)
953 vbox.addWidget(QRCodeWidget(seed))
956 b = QPushButton(_("OK"))
958 b.clicked.connect(d.accept)
965 def show_address_qrcode(self,address):
966 if not address: return
969 d.setWindowTitle(address)
970 d.setMinimumSize(270, 350)
972 qrw = QRCodeWidget(address)
976 amount_e = QLineEdit()
977 hbox.addWidget(QLabel(_('Amount')))
978 hbox.addWidget(amount_e)
981 #hbox = QHBoxLayout()
982 #label_e = QLineEdit()
983 #hbox.addWidget(QLabel('Label'))
984 #hbox.addWidget(label_e)
985 #vbox.addLayout(hbox)
987 def amount_changed():
988 amount = numbify(amount_e)
989 #label = str( label_e.getText() )
990 if amount is not None:
991 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
993 qrw.set_addr( address )
997 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
998 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
1000 amount_e.textChanged.connect( amount_changed )
1002 hbox = QHBoxLayout()
1004 b = QPushButton(_("Save"))
1005 b.clicked.connect(do_save)
1007 b = QPushButton(_("Close"))
1009 b.clicked.connect(d.accept)
1011 vbox.addLayout(hbox)
1015 def question(self, msg):
1016 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1018 def show_message(self, msg):
1019 QMessageBox.information(self, _('Message'), msg, _('OK'))
1021 def password_dialog(self ):
1028 vbox = QVBoxLayout()
1029 msg = _('Please enter your password')
1030 vbox.addWidget(QLabel(msg))
1032 grid = QGridLayout()
1034 grid.addWidget(QLabel(_('Password')), 1, 0)
1035 grid.addWidget(pw, 1, 1)
1036 vbox.addLayout(grid)
1038 vbox.addLayout(ok_cancel_buttons(d))
1041 if not d.exec_(): return
1042 return unicode(pw.text())
1049 def change_password_dialog( wallet, parent=None ):
1052 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1060 new_pw = QLineEdit()
1061 new_pw.setEchoMode(2)
1062 conf_pw = QLineEdit()
1063 conf_pw.setEchoMode(2)
1065 vbox = QVBoxLayout()
1067 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')
1069 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1070 vbox.addWidget(QLabel(msg))
1072 grid = QGridLayout()
1075 if wallet.use_encryption:
1076 grid.addWidget(QLabel(_('Password')), 1, 0)
1077 grid.addWidget(pw, 1, 1)
1079 grid.addWidget(QLabel(_('New Password')), 2, 0)
1080 grid.addWidget(new_pw, 2, 1)
1082 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1083 grid.addWidget(conf_pw, 3, 1)
1084 vbox.addLayout(grid)
1086 vbox.addLayout(ok_cancel_buttons(d))
1089 if not d.exec_(): return
1091 password = unicode(pw.text()) if wallet.use_encryption else None
1092 new_password = unicode(new_pw.text())
1093 new_password2 = unicode(conf_pw.text())
1096 seed = wallet.pw_decode( wallet.seed, password)
1098 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1101 if new_password != new_password2:
1102 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1105 wallet.update_password(seed, password, new_password)
1108 def seed_dialog(wallet, parent=None):
1112 vbox = QVBoxLayout()
1113 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1114 vbox.addWidget(QLabel(msg))
1116 grid = QGridLayout()
1119 seed_e = QLineEdit()
1120 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1121 grid.addWidget(seed_e, 1, 1)
1125 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1126 grid.addWidget(gap_e, 2, 1)
1127 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1128 vbox.addLayout(grid)
1130 vbox.addLayout(ok_cancel_buttons(d))
1133 if not d.exec_(): return
1136 gap = int(unicode(gap_e.text()))
1138 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1142 seed = unicode(seed_e.text())
1145 print_error("Warning: Not hex, trying decode")
1147 seed = mnemonic.mn_decode( seed.split(' ') )
1149 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1152 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1155 wallet.seed = str(seed)
1156 #print repr(wallet.seed)
1157 wallet.gap_limit = gap
1161 def set_expert_mode(self, b):
1162 self.wallet.expert_mode = b
1164 self.update_receive_tab()
1165 self.update_contacts_tab()
1166 # if self.wallet.seed:
1167 # self.nochange_cb.setHidden(not self.wallet.expert_mode)
1170 def settings_dialog(self):
1173 vbox = QVBoxLayout()
1174 msg = _('Here are the settings of your wallet.') + '\n'\
1175 + _('For more explanations, click on the help buttons next to each field.')
1178 label.setFixedWidth(250)
1179 label.setWordWrap(True)
1180 label.setAlignment(Qt.AlignJustify)
1181 vbox.addWidget(label)
1183 grid = QGridLayout()
1185 vbox.addLayout(grid)
1188 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1189 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1190 grid.addWidget(fee_e, 2, 1)
1191 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1192 + _('Recommended value') + ': 0.001'
1193 grid.addWidget(HelpButton(msg), 2, 2)
1194 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1197 nz_e.setText("%d"% self.wallet.num_zeros)
1198 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1199 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1200 grid.addWidget(HelpButton(msg), 3, 2)
1201 grid.addWidget(nz_e, 3, 1)
1202 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1204 cb = QCheckBox(_('Expert mode'))
1205 grid.addWidget(cb, 4, 0)
1206 cb.setChecked(self.wallet.expert_mode)
1208 if self.wallet.expert_mode:
1210 usechange_cb = QCheckBox(_('Use change addresses'))
1211 grid.addWidget(usechange_cb, 5, 0)
1212 usechange_cb.setChecked(self.wallet.use_change)
1213 grid.addWidget(HelpButton(_('Using a change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1215 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1216 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1217 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1218 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1219 + _('Warning') + ': ' \
1220 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1221 + _('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'
1223 gap_e.setText("%d"% self.wallet.gap_limit)
1224 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1225 grid.addWidget(gap_e, 6, 1)
1226 grid.addWidget(HelpButton(msg), 6, 2)
1227 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1230 vbox.addLayout(ok_cancel_buttons(d))
1234 if not d.exec_(): return
1236 fee = unicode(fee_e.text())
1238 fee = int( 100000000 * Decimal(fee) )
1240 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1243 if self.wallet.fee != fee:
1244 self.wallet.fee = fee
1247 nz = unicode(nz_e.text())
1252 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1255 if self.wallet.num_zeros != nz:
1256 self.wallet.num_zeros = nz
1257 self.update_history_tab()
1258 self.update_receive_tab()
1261 if self.wallet.expert_mode:
1263 self.wallet.use_change = usechange_cb.isChecked()
1266 n = int(gap_e.text())
1268 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1270 if self.wallet.gap_limit != n:
1271 r = self.wallet.change_gap_limit(n)
1273 self.update_receive_tab()
1275 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1277 self.set_expert_mode(cb.isChecked())
1281 def network_dialog(wallet, parent=None):
1282 interface = wallet.interface
1284 if interface.is_connected:
1285 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1287 status = _("Not connected")
1288 server = wallet.server
1291 status = _("Please choose a server.")
1292 server = random.choice( DEFAULT_SERVERS )
1294 if not wallet.interface.servers:
1296 for x in DEFAULT_SERVERS:
1297 h,port,protocol = x.split(':')
1298 servers_list.append( (h,[(protocol,port)] ) )
1300 servers_list = wallet.interface.servers
1303 for item in servers_list:
1307 protocol, port = item2
1313 d.setWindowTitle(_('Server'))
1314 d.setMinimumSize(375, 20)
1316 vbox = QVBoxLayout()
1319 hbox = QHBoxLayout()
1321 l.setPixmap(QPixmap(":icons/network.png"))
1323 hbox.addWidget(QLabel(status))
1325 vbox.addLayout(hbox)
1327 hbox = QHBoxLayout()
1328 host_line = QLineEdit()
1329 host_line.setText(server)
1330 hbox.addWidget(QLabel(_('Connect to') + ':'))
1331 hbox.addWidget(host_line)
1332 vbox.addLayout(hbox)
1334 hbox = QHBoxLayout()
1336 buttonGroup = QGroupBox(_("Protocol"))
1337 radio1 = QRadioButton("tcp", buttonGroup)
1338 radio2 = QRadioButton("http", buttonGroup)
1341 return unicode(host_line.text()).split(':')
1343 def set_button(protocol):
1345 radio1.setChecked(1)
1346 elif protocol == 'h':
1347 radio2.setChecked(1)
1349 def set_protocol(protocol):
1350 host = current_line()[0]
1352 if protocol not in pp.keys():
1353 protocol = pp.keys()[0]
1354 set_button(protocol)
1356 host_line.setText( host + ':' + port + ':' + protocol)
1358 radio1.clicked.connect(lambda x: set_protocol('t') )
1359 radio2.clicked.connect(lambda x: set_protocol('h') )
1361 set_button(current_line()[2])
1363 hbox.addWidget(QLabel(_('Protocol')+':'))
1364 hbox.addWidget(radio1)
1365 hbox.addWidget(radio2)
1367 vbox.addLayout(hbox)
1369 if wallet.interface.servers:
1370 label = _('Active Servers')
1372 label = _('Default Servers')
1374 servers_list_widget = QTreeWidget(parent)
1375 servers_list_widget.setHeaderLabels( [ label ] )
1376 servers_list_widget.setMaximumHeight(150)
1377 for host in plist.keys():
1378 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1381 host = unicode(x.text(0))
1383 if 't' in pp.keys():
1386 protocol = pp.keys()[0]
1388 host_line.setText( host + ':' + port + ':' + protocol)
1389 set_button(protocol)
1391 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1392 vbox.addWidget(servers_list_widget)
1394 vbox.addLayout(ok_cancel_buttons(d))
1397 if not d.exec_(): return
1398 server = unicode( host_line.text() )
1401 wallet.set_server(server)
1403 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1415 def __init__(self, wallet, app=None):
1416 self.wallet = wallet
1418 self.app = QApplication(sys.argv)
1420 def waiting_dialog(self):
1426 w.setWindowTitle('Electrum')
1428 vbox = QVBoxLayout()
1433 if self.wallet.up_to_date:
1436 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1437 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1439 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1440 self.wallet.interface.poke()
1445 def restore_or_create(self):
1447 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1448 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1449 if r==2: return False
1451 is_recovery = (r==1)
1452 wallet = self.wallet
1453 # ask for the server.
1454 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1457 wallet.new_seed(None)
1458 wallet.init_mpk( wallet.seed )
1459 wallet.up_to_date_event.clear()
1460 wallet.up_to_date = False
1461 self.waiting_dialog()
1462 # run a dialog indicating the seed, ask the user to remember it
1463 ElectrumWindow.show_seed_dialog(wallet)
1465 ElectrumWindow.change_password_dialog(wallet)
1467 # ask for seed and gap.
1468 if not ElectrumWindow.seed_dialog( wallet ): return False
1469 wallet.init_mpk( wallet.seed )
1470 wallet.up_to_date_event.clear()
1471 wallet.up_to_date = False
1472 self.waiting_dialog()
1473 if wallet.is_found():
1474 # history and addressbook
1475 wallet.update_tx_history()
1476 wallet.fill_addressbook()
1477 print "Recovery successful"
1480 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1488 w = ElectrumWindow(self.wallet)
1489 if url: w.set_url(url)