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, qrscanner
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' ), _( 'To / From' ) , _('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)
448 qrcode = qrscanner.scan_qr()
449 if 'address' in qrcode:
450 self.payto_e.setText(qrcode['address'])
451 if 'amount' in qrcode:
452 self.amount_e.setText(str(qrcode['amount']))
453 if 'label' in qrcode:
454 self.message_e.setText(qrcode['label'])
455 if 'message' in qrcode:
456 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
459 if qrscanner.is_available():
460 b = QPushButton(_("Scan QR code"))
461 b.clicked.connect(fill_from_qr)
462 grid.addWidget(b, 1, 5)
464 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)
466 completer = QCompleter()
467 completer.setCaseSensitivity(False)
468 self.payto_e.setCompleter(completer)
469 completer.setModel(self.completions)
471 self.message_e = QLineEdit()
472 grid.addWidget(QLabel(_('Description')), 2, 0)
473 grid.addWidget(self.message_e, 2, 1, 1, 3)
474 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)
476 self.amount_e = QLineEdit()
477 grid.addWidget(QLabel(_('Amount')), 3, 0)
478 grid.addWidget(self.amount_e, 3, 1, 1, 2)
479 grid.addWidget(HelpButton(
480 _('Amount to be sent.') + '\n\n' \
481 + _('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)
483 self.fee_e = QLineEdit()
484 grid.addWidget(QLabel(_('Fee')), 4, 0)
485 grid.addWidget(self.fee_e, 4, 1, 1, 2)
486 grid.addWidget(HelpButton(
487 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
488 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
489 + _('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)
491 b = EnterButton(_("Send"), self.do_send)
492 grid.addWidget(b, 6, 1)
494 b = EnterButton(_("Clear"),self.do_clear)
495 grid.addWidget(b, 6, 2)
497 self.payto_sig = QLabel('')
498 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
500 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
501 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
510 def entry_changed( is_fee ):
511 self.funds_error = False
512 amount = numbify(self.amount_e)
513 fee = numbify(self.fee_e)
514 if not is_fee: fee = None
517 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
519 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
522 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
525 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
526 self.funds_error = True
527 self.amount_e.setPalette(palette)
528 self.fee_e.setPalette(palette)
530 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
531 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
536 def update_completions(self):
538 for addr,label in self.wallet.labels.items():
539 if addr in self.wallet.addressbook:
540 l.append( label + ' <' + addr + '>')
541 l = l + self.wallet.aliases.keys()
543 self.completions.setStringList(l)
549 label = unicode( self.message_e.text() )
550 r = unicode( self.payto_e.text() )
554 m1 = re.match(ALIAS_REGEXP, r)
555 # label or alias, with address in brackets
556 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
559 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
563 to_address = m2.group(2)
567 if not self.wallet.is_valid(to_address):
568 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
572 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
574 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
577 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
579 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
582 if self.wallet.use_encryption:
583 password = self.password_dialog()
590 tx = self.wallet.mktx( to_address, amount, label, password, fee)
591 except BaseException, e:
592 self.show_message(str(e))
595 status, msg = self.wallet.sendtx( tx )
597 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
599 self.update_contacts_tab()
601 QMessageBox.warning(self, _('Error'), msg, _('OK'))
604 def set_url(self, url):
605 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
606 self.tabs.setCurrentIndex(1)
607 label = self.wallet.labels.get(payto)
608 m_addr = label + ' <'+ payto+'>' if label else payto
609 self.payto_e.setText(m_addr)
611 self.message_e.setText(message)
612 self.amount_e.setText(amount)
614 self.set_frozen(self.payto_e,True)
615 self.set_frozen(self.amount_e,True)
616 self.set_frozen(self.message_e,True)
617 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
619 self.payto_sig.setVisible(False)
622 self.payto_sig.setVisible(False)
623 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
625 self.set_frozen(e,False)
627 def set_frozen(self,entry,frozen):
629 entry.setReadOnly(True)
630 entry.setFrame(False)
632 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
633 entry.setPalette(palette)
635 entry.setReadOnly(False)
638 palette.setColor(entry.backgroundRole(), QColor('white'))
639 entry.setPalette(palette)
642 def toggle_freeze(self,addr):
644 if addr in self.wallet.frozen_addresses:
645 self.wallet.unfreeze(addr)
647 self.wallet.freeze(addr)
648 self.update_receive_tab()
650 def toggle_priority(self,addr):
652 if addr in self.wallet.prioritized_addresses:
653 self.wallet.unprioritize(addr)
655 self.wallet.prioritize(addr)
656 self.update_receive_tab()
659 def create_list_tab(self, headers):
660 "generic tab creation method"
661 l = MyTreeWidget(self)
662 l.setColumnCount( len(headers) )
663 l.setHeaderLabels( headers )
673 vbox.addWidget(buttons)
678 buttons.setLayout(hbox)
683 def create_receive_tab(self):
684 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
685 l.setContextMenuPolicy(Qt.CustomContextMenu)
686 l.customContextMenuRequested.connect(self.create_receive_menu)
687 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
688 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
689 self.receive_list = l
690 self.receive_buttons_hbox = hbox
694 def create_contacts_tab(self):
695 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
696 l.setContextMenuPolicy(Qt.CustomContextMenu)
697 l.customContextMenuRequested.connect(self.create_contact_menu)
698 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
699 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
700 self.contacts_list = l
701 self.contacts_buttons_hbox = hbox
702 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
707 def create_receive_menu(self, position):
708 # fixme: this function apparently has a side effect.
709 # if it is not called the menu pops up several times
710 #self.receive_list.selectedIndexes()
712 item = self.receive_list.itemAt(position)
714 addr = unicode(item.text(1))
716 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
717 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
718 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
719 if self.wallet.expert_mode:
720 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
721 menu.addAction(t, lambda: self.toggle_freeze(addr))
722 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
723 menu.addAction(t, lambda: self.toggle_priority(addr))
724 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
727 def payto(self, x, is_alias):
734 label = self.wallet.labels.get(addr)
735 m_addr = label + ' <' + addr + '>' if label else addr
736 self.tabs.setCurrentIndex(1)
737 self.payto_e.setText(m_addr)
738 self.amount_e.setFocus()
740 def delete_contact(self, x, is_alias):
741 if self.question("Do you want to remove %s from your list of contacts?"%x):
742 if not is_alias and x in self.wallet.addressbook:
743 self.wallet.addressbook.remove(x)
744 if x in self.wallet.labels.keys():
745 self.wallet.labels.pop(x)
746 elif is_alias and x in self.wallet.aliases:
747 self.wallet.aliases.pop(x)
748 self.update_history_tab()
749 self.update_contacts_tab()
750 self.update_completions()
752 def create_contact_menu(self, position):
753 # fixme: this function apparently has a side effect.
754 # if it is not called the menu pops up several times
755 #self.contacts_list.selectedIndexes()
757 item = self.contacts_list.itemAt(position)
759 addr = unicode(item.text(0))
760 label = unicode(item.text(1))
761 is_alias = label in self.wallet.aliases.keys()
762 x = label if is_alias else addr
764 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
765 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
766 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
768 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
770 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
771 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
772 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
775 def update_receive_tab(self):
776 l = self.receive_list
778 l.setColumnHidden(0,not self.wallet.expert_mode)
779 l.setColumnHidden(3,not self.wallet.expert_mode)
780 l.setColumnHidden(4,not self.wallet.expert_mode)
781 l.setColumnWidth(0, 50)
782 l.setColumnWidth(1, 310)
783 l.setColumnWidth(2, 250)
784 l.setColumnWidth(3, 130)
785 l.setColumnWidth(4, 10)
789 for address in self.wallet.all_addresses():
791 if self.wallet.is_change(address) and not self.wallet.expert_mode:
794 label = self.wallet.labels.get(address,'')
796 h = self.wallet.history.get(address,[])
798 if not item['is_input'] : n=n+1
802 if address in self.wallet.addresses:
804 if gap > self.wallet.gap_limit:
807 if address in self.wallet.addresses:
810 c, u = self.wallet.get_addr_balance(address)
811 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
812 flags = self.wallet.get_address_flags(address)
813 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
815 item.setFont(0, QFont(MONOSPACE_FONT))
816 item.setFont(1, QFont(MONOSPACE_FONT))
817 item.setFont(3, QFont(MONOSPACE_FONT))
818 if address in self.wallet.frozen_addresses:
819 item.setBackgroundColor(1, QColor('lightblue'))
820 elif address in self.wallet.prioritized_addresses:
821 item.setBackgroundColor(1, QColor('lightgreen'))
822 if is_red and address in self.wallet.addresses:
823 item.setBackgroundColor(1, QColor('red'))
824 l.addTopLevelItem(item)
826 # we use column 1 because column 0 may be hidden
827 l.setCurrentItem(l.topLevelItem(0),1)
829 def show_contact_details(self, m):
830 a = self.wallet.aliases.get(m)
832 if a[0] in self.wallet.authorities.keys():
833 s = self.wallet.authorities.get(a[0])
836 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
837 QMessageBox.information(self, 'Alias', msg, 'OK')
839 def update_contacts_tab(self):
841 l = self.contacts_list
843 l.setColumnHidden(2, not self.wallet.expert_mode)
844 l.setColumnWidth(0, 350)
845 l.setColumnWidth(1, 330)
846 l.setColumnWidth(2, 100)
849 for alias, v in self.wallet.aliases.items():
851 alias_targets.append(target)
852 item = QTreeWidgetItem( [ target, alias, '-'] )
853 item.setBackgroundColor(0, QColor('lightgray'))
854 l.addTopLevelItem(item)
856 for address in self.wallet.addressbook:
857 if address in alias_targets: continue
858 label = self.wallet.labels.get(address,'')
860 for item in self.wallet.tx_history.values():
861 if address in item['outputs'] : n=n+1
863 item = QTreeWidgetItem( [ address, label, tx] )
864 item.setFont(0, QFont(MONOSPACE_FONT))
865 l.addTopLevelItem(item)
867 l.setCurrentItem(l.topLevelItem(0))
869 def create_wall_tab(self):
870 self.textbox = textbox = QTextEdit(self)
871 textbox.setFont(QFont(MONOSPACE_FONT))
872 textbox.setReadOnly(True)
875 def create_status_bar(self):
877 sb.setFixedHeight(35)
879 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
880 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
882 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
883 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
884 sb.addPermanentWidget( self.status_button )
885 self.setStatusBar(sb)
887 def new_contact_dialog(self):
888 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
889 address = unicode(text)
891 if self.wallet.is_valid(address):
892 self.wallet.addressbook.append(address)
894 self.update_contacts_tab()
895 self.update_history_tab()
896 self.update_completions()
898 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
901 def show_seed_dialog(wallet, parent=None):
904 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
907 if wallet.use_encryption:
908 password = parent.password_dialog()
909 if not password: return
914 seed = wallet.pw_decode( wallet.seed, password)
916 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
919 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
920 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
921 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
922 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
926 d.setWindowTitle(_("Seed"))
927 d.setMinimumSize(400, 270)
931 vbox2 = QVBoxLayout()
933 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
936 hbox.addLayout(vbox2)
937 hbox.addWidget(QLabel(msg))
949 b = QPushButton(_("Copy to Clipboard"))
950 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
952 b = QPushButton(_("View as QR Code"))
953 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
956 b = QPushButton(_("OK"))
957 b.clicked.connect(d.accept)
964 def show_seed_qrcode(seed):
968 d.setWindowTitle(_("Seed"))
969 d.setMinimumSize(270, 300)
971 vbox.addWidget(QRCodeWidget(seed))
974 b = QPushButton(_("OK"))
976 b.clicked.connect(d.accept)
983 def show_address_qrcode(self,address):
984 if not address: return
987 d.setWindowTitle(address)
988 d.setMinimumSize(270, 350)
990 qrw = QRCodeWidget(address)
994 amount_e = QLineEdit()
995 hbox.addWidget(QLabel(_('Amount')))
996 hbox.addWidget(amount_e)
999 #hbox = QHBoxLayout()
1000 #label_e = QLineEdit()
1001 #hbox.addWidget(QLabel('Label'))
1002 #hbox.addWidget(label_e)
1003 #vbox.addLayout(hbox)
1005 def amount_changed():
1006 amount = numbify(amount_e)
1007 #label = str( label_e.getText() )
1008 if amount is not None:
1009 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
1011 qrw.set_addr( address )
1015 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
1016 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
1018 amount_e.textChanged.connect( amount_changed )
1020 hbox = QHBoxLayout()
1022 b = QPushButton(_("Save"))
1023 b.clicked.connect(do_save)
1025 b = QPushButton(_("Close"))
1027 b.clicked.connect(d.accept)
1029 vbox.addLayout(hbox)
1033 def question(self, msg):
1034 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1036 def show_message(self, msg):
1037 QMessageBox.information(self, _('Message'), msg, _('OK'))
1039 def password_dialog(self ):
1046 vbox = QVBoxLayout()
1047 msg = _('Please enter your password')
1048 vbox.addWidget(QLabel(msg))
1050 grid = QGridLayout()
1052 grid.addWidget(QLabel(_('Password')), 1, 0)
1053 grid.addWidget(pw, 1, 1)
1054 vbox.addLayout(grid)
1056 vbox.addLayout(ok_cancel_buttons(d))
1059 if not d.exec_(): return
1060 return unicode(pw.text())
1067 def change_password_dialog( wallet, parent=None ):
1070 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1078 new_pw = QLineEdit()
1079 new_pw.setEchoMode(2)
1080 conf_pw = QLineEdit()
1081 conf_pw.setEchoMode(2)
1083 vbox = QVBoxLayout()
1085 msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'+_('To disable wallet encryption, enter an empty new password.')) if wallet.use_encryption else _('Your wallet keys are not encrypted')
1087 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1088 vbox.addWidget(QLabel(msg))
1090 grid = QGridLayout()
1093 if wallet.use_encryption:
1094 grid.addWidget(QLabel(_('Password')), 1, 0)
1095 grid.addWidget(pw, 1, 1)
1097 grid.addWidget(QLabel(_('New Password')), 2, 0)
1098 grid.addWidget(new_pw, 2, 1)
1100 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1101 grid.addWidget(conf_pw, 3, 1)
1102 vbox.addLayout(grid)
1104 vbox.addLayout(ok_cancel_buttons(d))
1107 if not d.exec_(): return
1109 password = unicode(pw.text()) if wallet.use_encryption else None
1110 new_password = unicode(new_pw.text())
1111 new_password2 = unicode(conf_pw.text())
1114 seed = wallet.pw_decode( wallet.seed, password)
1116 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1119 if new_password != new_password2:
1120 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1123 wallet.update_password(seed, password, new_password)
1126 def seed_dialog(wallet, parent=None):
1130 vbox = QVBoxLayout()
1131 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1132 vbox.addWidget(QLabel(msg))
1134 grid = QGridLayout()
1137 seed_e = QLineEdit()
1138 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1139 grid.addWidget(seed_e, 1, 1)
1143 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1144 grid.addWidget(gap_e, 2, 1)
1145 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1146 vbox.addLayout(grid)
1148 vbox.addLayout(ok_cancel_buttons(d))
1151 if not d.exec_(): return
1154 gap = int(unicode(gap_e.text()))
1156 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1160 seed = unicode(seed_e.text())
1163 print_error("Warning: Not hex, trying decode")
1165 seed = mnemonic.mn_decode( seed.split(' ') )
1167 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1170 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1173 wallet.seed = str(seed)
1174 #print repr(wallet.seed)
1175 wallet.gap_limit = gap
1179 def set_expert_mode(self, b):
1180 self.wallet.expert_mode = b
1182 self.update_receive_tab()
1183 self.update_contacts_tab()
1184 # if self.wallet.seed:
1185 # self.nochange_cb.setHidden(not self.wallet.expert_mode)
1188 def settings_dialog(self):
1191 vbox = QVBoxLayout()
1192 msg = _('Here are the settings of your wallet.') + '\n'\
1193 + _('For more explanations, click on the help buttons next to each field.')
1196 label.setFixedWidth(250)
1197 label.setWordWrap(True)
1198 label.setAlignment(Qt.AlignJustify)
1199 vbox.addWidget(label)
1201 grid = QGridLayout()
1203 vbox.addLayout(grid)
1206 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1207 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1208 grid.addWidget(fee_e, 2, 1)
1209 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1210 + _('Recommended value') + ': 0.001'
1211 grid.addWidget(HelpButton(msg), 2, 2)
1212 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1215 nz_e.setText("%d"% self.wallet.num_zeros)
1216 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1217 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1218 grid.addWidget(HelpButton(msg), 3, 2)
1219 grid.addWidget(nz_e, 3, 1)
1220 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1222 cb = QCheckBox(_('Expert mode'))
1223 grid.addWidget(cb, 4, 0)
1224 cb.setChecked(self.wallet.expert_mode)
1226 if self.wallet.expert_mode:
1228 usechange_cb = QCheckBox(_('Use change addresses'))
1229 grid.addWidget(usechange_cb, 5, 0)
1230 usechange_cb.setChecked(self.wallet.use_change)
1231 grid.addWidget(HelpButton(_('Using a change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1233 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1234 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1235 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1236 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1237 + _('Warning') + ': ' \
1238 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1239 + _('Do not modify it if you do not understand what you are doing, or if you expect to recover your wallet without knowing it!') + '\n\n'
1241 gap_e.setText("%d"% self.wallet.gap_limit)
1242 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1243 grid.addWidget(gap_e, 6, 1)
1244 grid.addWidget(HelpButton(msg), 6, 2)
1245 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1248 vbox.addLayout(ok_cancel_buttons(d))
1252 if not d.exec_(): return
1254 fee = unicode(fee_e.text())
1256 fee = int( 100000000 * Decimal(fee) )
1258 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1261 if self.wallet.fee != fee:
1262 self.wallet.fee = fee
1265 nz = unicode(nz_e.text())
1270 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1273 if self.wallet.num_zeros != nz:
1274 self.wallet.num_zeros = nz
1275 self.update_history_tab()
1276 self.update_receive_tab()
1279 if self.wallet.expert_mode:
1281 self.wallet.use_change = usechange_cb.isChecked()
1284 n = int(gap_e.text())
1286 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1288 if self.wallet.gap_limit != n:
1289 r = self.wallet.change_gap_limit(n)
1291 self.update_receive_tab()
1293 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1295 self.set_expert_mode(cb.isChecked())
1299 def network_dialog(wallet, parent=None):
1300 interface = wallet.interface
1302 if interface.is_connected:
1303 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1305 status = _("Not connected")
1306 server = wallet.server
1309 status = _("Please choose a server.")
1310 server = random.choice( DEFAULT_SERVERS )
1312 if not wallet.interface.servers:
1314 for x in DEFAULT_SERVERS:
1315 h,port,protocol = x.split(':')
1316 servers_list.append( (h,[(protocol,port)] ) )
1318 servers_list = wallet.interface.servers
1321 for item in servers_list:
1325 protocol, port = item2
1331 d.setWindowTitle(_('Server'))
1332 d.setMinimumSize(375, 20)
1334 vbox = QVBoxLayout()
1337 hbox = QHBoxLayout()
1339 l.setPixmap(QPixmap(":icons/network.png"))
1341 hbox.addWidget(QLabel(status))
1343 vbox.addLayout(hbox)
1345 hbox = QHBoxLayout()
1346 host_line = QLineEdit()
1347 host_line.setText(server)
1348 hbox.addWidget(QLabel(_('Connect to') + ':'))
1349 hbox.addWidget(host_line)
1350 vbox.addLayout(hbox)
1352 hbox = QHBoxLayout()
1354 buttonGroup = QGroupBox(_("Protocol"))
1355 radio1 = QRadioButton("tcp", buttonGroup)
1356 radio2 = QRadioButton("http", buttonGroup)
1359 return unicode(host_line.text()).split(':')
1361 def set_button(protocol):
1363 radio1.setChecked(1)
1364 elif protocol == 'h':
1365 radio2.setChecked(1)
1367 def set_protocol(protocol):
1368 host = current_line()[0]
1370 if protocol not in pp.keys():
1371 protocol = pp.keys()[0]
1372 set_button(protocol)
1374 host_line.setText( host + ':' + port + ':' + protocol)
1376 radio1.clicked.connect(lambda x: set_protocol('t') )
1377 radio2.clicked.connect(lambda x: set_protocol('h') )
1379 set_button(current_line()[2])
1381 hbox.addWidget(QLabel(_('Protocol')+':'))
1382 hbox.addWidget(radio1)
1383 hbox.addWidget(radio2)
1385 vbox.addLayout(hbox)
1387 if wallet.interface.servers:
1388 label = _('Active Servers')
1390 label = _('Default Servers')
1392 servers_list_widget = QTreeWidget(parent)
1393 servers_list_widget.setHeaderLabels( [ label ] )
1394 servers_list_widget.setMaximumHeight(150)
1395 for host in plist.keys():
1396 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1399 host = unicode(x.text(0))
1401 if 't' in pp.keys():
1404 protocol = pp.keys()[0]
1406 host_line.setText( host + ':' + port + ':' + protocol)
1407 set_button(protocol)
1409 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1410 vbox.addWidget(servers_list_widget)
1412 vbox.addLayout(ok_cancel_buttons(d))
1415 if not d.exec_(): return
1416 server = unicode( host_line.text() )
1419 wallet.set_server(server)
1421 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1433 def __init__(self, wallet, app=None):
1434 self.wallet = wallet
1436 self.app = QApplication(sys.argv)
1438 def waiting_dialog(self):
1444 w.setWindowTitle('Electrum')
1446 vbox = QVBoxLayout()
1451 if self.wallet.up_to_date:
1454 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1455 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1457 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1458 self.wallet.interface.poke()
1463 def restore_or_create(self):
1465 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1466 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1467 if r==2: return False
1469 is_recovery = (r==1)
1470 wallet = self.wallet
1471 # ask for the server.
1472 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1475 wallet.new_seed(None)
1476 wallet.init_mpk( wallet.seed )
1477 wallet.up_to_date_event.clear()
1478 wallet.up_to_date = False
1479 self.waiting_dialog()
1480 # run a dialog indicating the seed, ask the user to remember it
1481 ElectrumWindow.show_seed_dialog(wallet)
1483 ElectrumWindow.change_password_dialog(wallet)
1485 # ask for seed and gap.
1486 if not ElectrumWindow.seed_dialog( wallet ): return False
1487 wallet.init_mpk( wallet.seed )
1488 wallet.up_to_date_event.clear()
1489 wallet.up_to_date = False
1490 self.waiting_dialog()
1491 if wallet.is_found():
1492 # history and addressbook
1493 wallet.update_tx_history()
1494 wallet.fill_addressbook()
1495 print "Recovery successful"
1498 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1506 w = ElectrumWindow(self.wallet)
1507 if url: w.set_url(url)