3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re
25 print "could not import PyQt4"
26 print "on Linux systems, you may try 'sudo apt-get install python-qt4'"
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
32 import PyQt4.QtGui as QtGui
33 from interface import DEFAULT_SERVERS
38 print "Could not import icons_rc.py"
39 print "Please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'"
42 from wallet import format_satoshis
43 import bmp, mnemonic, pyqrnative
45 from decimal import Decimal
49 if platform.system() == 'Windows':
50 MONOSPACE_FONT = 'Lucida Console'
51 elif platform.system() == 'Darwin':
52 MONOSPACE_FONT = 'Monaco'
54 MONOSPACE_FONT = 'monospace'
56 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
58 def numbify(entry, is_int = False):
59 text = unicode(entry.text()).strip()
61 if not is_int: chars +='.'
62 s = ''.join([i for i in text if i in chars])
67 s = s[:p] + '.' + s[p:p+8]
69 amount = int( Decimal(s) * 100000000 )
81 class Timer(QtCore.QThread):
84 self.emit(QtCore.SIGNAL('timersignal'))
87 class HelpButton(QPushButton):
88 def __init__(self, text):
89 QPushButton.__init__(self, '?')
90 self.setFocusPolicy(Qt.NoFocus)
91 self.setFixedWidth(20)
92 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
95 class EnterButton(QPushButton):
96 def __init__(self, text, func):
97 QPushButton.__init__(self, text)
99 self.clicked.connect(func)
101 def keyPressEvent(self, e):
102 if e.key() == QtCore.Qt.Key_Return:
105 class MyTreeWidget(QTreeWidget):
106 def __init__(self, parent):
107 QTreeWidget.__init__(self, parent)
110 for i in range(0,self.viewport().height()/5):
111 if self.itemAt(QPoint(0,i*5)) == item:
115 for j in range(0,30):
116 if self.itemAt(QPoint(0,i*5 + j)) != item:
118 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
120 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
125 class StatusBarButton(QPushButton):
126 def __init__(self, icon, tooltip, func):
127 QPushButton.__init__(self, icon, '')
128 self.setToolTip(tooltip)
130 self.setMaximumWidth(25)
131 self.clicked.connect(func)
134 def keyPressEvent(self, e):
135 if e.key() == QtCore.Qt.Key_Return:
139 class QRCodeWidget(QWidget):
141 def __init__(self, addr):
142 super(QRCodeWidget, self).__init__()
143 self.setGeometry(300, 300, 350, 350)
146 def set_addr(self, addr):
148 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
149 self.qr.addData(addr)
152 def paintEvent(self, e):
153 qp = QtGui.QPainter()
156 size = self.qr.getModuleCount()*boxsize
157 k = self.qr.getModuleCount()
158 black = QColor(0, 0, 0, 255)
159 white = QColor(255, 255, 255, 255)
162 if self.qr.isDark(r, c):
168 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
173 def ok_cancel_buttons(dialog):
176 b = QPushButton("OK")
178 b.clicked.connect(dialog.accept)
179 b = QPushButton("Cancel")
181 b.clicked.connect(dialog.reject)
185 class ElectrumWindow(QMainWindow):
187 def __init__(self, wallet):
188 QMainWindow.__init__(self)
190 self.wallet.register_callback(self.update_callback)
192 self.funds_error = False
193 self.completions = QStringListModel()
195 self.tabs = tabs = QTabWidget(self)
196 tabs.addTab(self.create_history_tab(), _('History') )
198 tabs.addTab(self.create_send_tab(), _('Send') )
199 tabs.addTab(self.create_receive_tab(), _('Receive') )
200 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
201 tabs.addTab(self.create_wall_tab(), _('Wall') )
202 tabs.setMinimumSize(600, 400)
203 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
204 self.setCentralWidget(tabs)
205 self.create_status_bar()
206 self.setGeometry(100,100,840,400)
207 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
208 if not self.wallet.seed: title += ' [seedless]'
209 self.setWindowTitle( title )
211 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
212 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
213 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
214 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
216 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
217 self.history_list.setFocus(True)
219 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
220 if platform.system() == 'Windows':
221 n = 3 if self.wallet.seed else 2
222 tabs.setCurrentIndex (n)
223 tabs.setCurrentIndex (0)
226 def connect_slots(self, sender):
228 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
229 self.previous_payto_e=''
231 def check_recipient(self):
232 if self.payto_e.hasFocus():
234 r = unicode( self.payto_e.text() )
235 if r != self.previous_payto_e:
236 self.previous_payto_e = r
238 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
240 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
244 s = r + ' <' + to_address + '>'
245 self.payto_e.setText(s)
248 def update_callback(self):
249 self.emit(QtCore.SIGNAL('updatesignal'))
251 def update_wallet(self):
252 if self.wallet.interface and self.wallet.interface.is_connected:
253 if self.wallet.blocks == -1:
254 text = _( "Connecting..." )
255 icon = QIcon(":icons/status_disconnected.png")
256 elif self.wallet.blocks == 0:
257 text = _( "Server not ready" )
258 icon = QIcon(":icons/status_disconnected.png")
259 elif not self.wallet.up_to_date:
260 text = _( "Synchronizing..." )
261 icon = QIcon(":icons/status_waiting.png")
263 c, u = self.wallet.get_balance()
264 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
265 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
266 icon = QIcon(":icons/status_connected.png")
268 text = _( "Not connected" )
269 icon = QIcon(":icons/status_disconnected.png")
272 text = _( "Not enough funds" )
274 self.statusBar().showMessage(text)
275 self.status_button.setIcon( icon )
277 if self.wallet.up_to_date:
278 self.textbox.setText( self.wallet.banner )
279 self.update_history_tab()
280 self.update_receive_tab()
281 self.update_contacts_tab()
282 self.update_completions()
285 def create_history_tab(self):
286 self.history_list = l = MyTreeWidget(self)
288 l.setColumnWidth(0, 40)
289 l.setColumnWidth(1, 140)
290 l.setColumnWidth(2, 350)
291 l.setColumnWidth(3, 140)
292 l.setColumnWidth(4, 140)
293 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
294 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
295 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
296 l.setContextMenuPolicy(Qt.CustomContextMenu)
297 l.customContextMenuRequested.connect(self.create_history_menu)
300 def create_history_menu(self, position):
301 self.history_list.selectedIndexes()
302 item = self.history_list.currentItem()
304 tx_hash = str(item.toolTip(0))
306 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
307 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
308 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
309 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
311 def tx_details(self, tx_hash):
312 tx = self.wallet.tx_history.get(tx_hash)
315 conf = self.wallet.blocks - tx['height'] + 1
316 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
321 tx_details = _("Transaction Details") +"\n\n" \
322 + "Transaction ID:\n" + tx_hash + "\n\n" \
323 + "Status: %d confirmations\n\n"%conf \
324 + "Date: %s\n\n"%time_str \
325 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
326 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
328 r = self.wallet.receipts.get(tx_hash)
330 tx_details += "\n_______________________________________" \
331 + '\n\nSigned URI: ' + r[2] \
332 + "\n\nSigned by: " + r[0] \
333 + '\n\nSignature: ' + r[1]
335 QMessageBox.information(self, 'Details', tx_details, 'OK')
338 def tx_label_clicked(self, item, column):
339 if column==2 and item.isSelected():
340 tx_hash = str(item.toolTip(0))
342 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
343 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
344 self.history_list.editItem( item, column )
345 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
348 def tx_label_changed(self, item, column):
352 tx_hash = str(item.toolTip(0))
353 tx = self.wallet.tx_history.get(tx_hash)
354 s = self.wallet.labels.get(tx_hash)
355 text = unicode( item.text(2) )
357 self.wallet.labels[tx_hash] = text
358 item.setForeground(2, QBrush(QColor('black')))
360 if s: self.wallet.labels.pop(tx_hash)
361 text = tx['default_label']
362 item.setText(2, text)
363 item.setForeground(2, QBrush(QColor('gray')))
366 def edit_label(self, is_recv):
367 l = self.receive_list if is_recv else self.contacts_list
368 c = 2 if is_recv else 1
369 item = l.currentItem()
370 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
371 l.editItem( item, c )
372 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
374 def address_label_clicked(self, item, column, l, column_addr, column_label):
375 if column==column_label and item.isSelected():
376 addr = unicode( item.text(column_addr) )
377 label = unicode( item.text(column_label) )
378 if label in self.wallet.aliases.keys():
380 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
381 l.editItem( item, column )
382 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
384 def address_label_changed(self, item, column, l, column_addr, column_label):
385 addr = unicode( item.text(column_addr) )
386 text = unicode( item.text(column_label) )
388 if text not in self.wallet.aliases.keys():
389 self.wallet.labels[addr] = text
391 print "error: this is one of your aliases"
392 label = self.wallet.labels.get(addr,'')
393 item.setText(column_label, QString(label))
395 s = self.wallet.labels.get(addr)
396 if s: self.wallet.labels.pop(addr)
398 self.update_history_tab()
399 self.update_completions()
401 def update_history_tab(self):
402 self.history_list.clear()
404 for tx in self.wallet.get_tx_history():
405 tx_hash = tx['tx_hash']
407 conf = self.wallet.blocks - tx['height'] + 1
408 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
409 icon = QIcon(":icons/confirmed.png")
413 icon = QIcon(":icons/unconfirmed.png")
416 label = self.wallet.labels.get(tx_hash)
417 is_default_label = (label == '') or (label is None)
418 if is_default_label: label = tx['default_label']
420 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
421 item.setFont(2, QFont(MONOSPACE_FONT))
422 item.setFont(3, QFont(MONOSPACE_FONT))
423 item.setFont(4, QFont(MONOSPACE_FONT))
424 item.setToolTip(0, tx_hash)
426 item.setForeground(2, QBrush(QColor('grey')))
428 item.setIcon(0, icon)
429 self.history_list.insertTopLevelItem(0,item)
431 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
434 def create_send_tab(self):
439 grid.setColumnMinimumWidth(3,300)
440 grid.setColumnStretch(5,1)
442 self.payto_e = QLineEdit()
443 grid.addWidget(QLabel(_('Pay to')), 1, 0)
444 grid.addWidget(self.payto_e, 1, 1, 1, 3)
445 grid.addWidget(HelpButton(_('Recipient of the funds.') + '\n\n' + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)')), 1, 4)
447 completer = QCompleter()
448 completer.setCaseSensitivity(False)
449 self.payto_e.setCompleter(completer)
450 completer.setModel(self.completions)
452 self.message_e = QLineEdit()
453 grid.addWidget(QLabel(_('Description')), 2, 0)
454 grid.addWidget(self.message_e, 2, 1, 1, 3)
455 grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).') + '\n\n' + _('The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.')), 2, 4)
457 self.amount_e = QLineEdit()
458 grid.addWidget(QLabel(_('Amount')), 3, 0)
459 grid.addWidget(self.amount_e, 3, 1, 1, 2)
460 grid.addWidget(HelpButton(
461 _('Amount to be sent.') + '\n\n' \
462 + _('The amount will be displayed in red if you do not have enough funds in your wallet. Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.')), 3, 3)
464 self.fee_e = QLineEdit()
465 grid.addWidget(QLabel(_('Fee')), 4, 0)
466 grid.addWidget(self.fee_e, 4, 1, 1, 2)
467 grid.addWidget(HelpButton(
468 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
469 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
470 + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
472 b = EnterButton(_("Send"), self.do_send)
473 grid.addWidget(b, 6, 1)
475 b = EnterButton(_("Clear"),self.do_clear)
476 grid.addWidget(b, 6, 2)
478 self.payto_sig = QLabel('')
479 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
481 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
482 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
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 "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)