3 # Electrum - lightweight Bitcoin client
4 # Copyright (C) 2012 thomasv@gitorious
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import sys, time, datetime, re
25 print "could not import PyQt4"
26 print "on Linux systems, you may try 'sudo apt-get install python-qt4'"
29 from PyQt4.QtGui import *
30 from PyQt4.QtCore import *
31 import PyQt4.QtCore as QtCore
32 import PyQt4.QtGui as QtGui
33 from interface import DEFAULT_SERVERS
38 print "Could not import icons_rp.py"
39 print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'"
42 from wallet import format_satoshis
43 import bmp, mnemonic, pyqrnative
45 from decimal import Decimal
48 MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
49 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
51 def numbify(entry, is_int = False):
52 text = unicode(entry.text()).strip()
54 if not is_int: chars +='.'
55 s = ''.join([i for i in text if i in chars])
60 s = s[:p] + '.' + s[p:p+8]
62 amount = int( Decimal(s) * 100000000 )
74 class Timer(QtCore.QThread):
77 self.emit(QtCore.SIGNAL('timersignal'))
80 class HelpButton(QPushButton):
81 def __init__(self, text):
82 QPushButton.__init__(self, '?')
83 self.setFocusPolicy(Qt.NoFocus)
84 self.setFixedWidth(20)
85 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
88 class EnterButton(QPushButton):
89 def __init__(self, text, func):
90 QPushButton.__init__(self, text)
92 self.clicked.connect(func)
94 def keyPressEvent(self, e):
95 if e.key() == QtCore.Qt.Key_Return:
98 class MyTreeWidget(QTreeWidget):
99 def __init__(self, parent):
100 QTreeWidget.__init__(self, parent)
103 for i in range(0,self.viewport().height()/5):
104 if self.itemAt(QPoint(0,i*5)) == item:
108 for j in range(0,30):
109 if self.itemAt(QPoint(0,i*5 + j)) != item:
111 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
113 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
118 class StatusBarButton(QPushButton):
119 def __init__(self, icon, tooltip, func):
120 QPushButton.__init__(self, icon, '')
121 self.setToolTip(tooltip)
123 self.setMaximumWidth(25)
124 self.clicked.connect(func)
127 def keyPressEvent(self, e):
128 if e.key() == QtCore.Qt.Key_Return:
132 class QRCodeWidget(QWidget):
134 def __init__(self, addr):
135 super(QRCodeWidget, self).__init__()
136 self.setGeometry(300, 300, 350, 350)
139 def set_addr(self, addr):
141 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
142 self.qr.addData(addr)
145 def paintEvent(self, e):
146 qp = QtGui.QPainter()
149 size = self.qr.getModuleCount()*boxsize
150 k = self.qr.getModuleCount()
151 black = QColor(0, 0, 0, 255)
152 white = QColor(255, 255, 255, 255)
155 if self.qr.isDark(r, c):
161 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
166 def ok_cancel_buttons(dialog):
169 b = QPushButton("OK")
171 b.clicked.connect(dialog.accept)
172 b = QPushButton("Cancel")
174 b.clicked.connect(dialog.reject)
178 class ElectrumWindow(QMainWindow):
180 def __init__(self, wallet):
181 QMainWindow.__init__(self)
183 self.wallet.gui_callback = self.update_callback
185 self.funds_error = False
186 self.completions = QStringListModel()
188 self.tabs = tabs = QTabWidget(self)
189 tabs.addTab(self.create_history_tab(), _('History') )
191 tabs.addTab(self.create_send_tab(), _('Send') )
192 tabs.addTab(self.create_receive_tab(), _('Receive') )
193 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
194 tabs.addTab(self.create_wall_tab(), _('Wall') )
195 tabs.setMinimumSize(600, 400)
196 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
197 self.setCentralWidget(tabs)
198 self.create_status_bar()
199 self.setGeometry(100,100,840,400)
200 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
201 if not self.wallet.seed: title += ' [seedless]'
202 self.setWindowTitle( title )
205 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
206 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
207 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
208 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
210 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
211 self.history_list.setFocus(True)
213 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
214 if platform.system() == 'Windows':
215 n = 3 if self.wallet.seed else 2
216 tabs.setCurrentIndex (n)
217 tabs.setCurrentIndex (0)
220 def connect_slots(self, sender):
222 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
223 self.previous_payto_e=''
225 def check_recipient(self):
226 if self.payto_e.hasFocus():
228 r = unicode( self.payto_e.text() )
229 if r != self.previous_payto_e:
230 self.previous_payto_e = r
232 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
234 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
238 s = r + ' <' + to_address + '>'
239 self.payto_e.setText(s)
242 def update_callback(self):
243 self.emit(QtCore.SIGNAL('updatesignal'))
245 def update_wallet(self):
246 if self.wallet.interface and self.wallet.interface.is_connected:
247 if self.wallet.blocks == -1:
248 text = _( "Connecting..." )
249 icon = QIcon(":icons/status_disconnected.png")
250 elif self.wallet.blocks == 0:
251 text = _( "Server not ready" )
252 icon = QIcon(":icons/status_disconnected.png")
253 elif not self.wallet.up_to_date:
254 text = _( "Synchronizing..." )
255 icon = QIcon(":icons/status_waiting.png")
257 c, u = self.wallet.get_balance()
258 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
259 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
260 icon = QIcon(":icons/status_connected.png")
262 text = _( "Not connected" )
263 icon = QIcon(":icons/status_disconnected.png")
266 text = _( "Not enough funds" )
268 self.statusBar().showMessage(text)
269 self.status_button.setIcon( icon )
271 if self.wallet.up_to_date:
272 self.textbox.setText( self.wallet.banner )
273 self.update_history_tab()
274 self.update_receive_tab()
275 self.update_contacts_tab()
276 self.update_completions()
279 def create_history_tab(self):
280 self.history_list = l = MyTreeWidget(self)
282 l.setColumnWidth(0, 40)
283 l.setColumnWidth(1, 140)
284 l.setColumnWidth(2, 350)
285 l.setColumnWidth(3, 140)
286 l.setColumnWidth(4, 140)
287 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
288 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
289 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
290 l.setContextMenuPolicy(Qt.CustomContextMenu)
291 l.customContextMenuRequested.connect(self.create_history_menu)
294 def create_history_menu(self, position):
295 self.history_list.selectedIndexes()
296 item = self.history_list.currentItem()
298 tx_hash = str(item.toolTip(0))
300 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
301 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
302 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
303 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
305 def tx_details(self, tx_hash):
306 tx = self.wallet.tx_history.get(tx_hash)
309 conf = self.wallet.blocks - tx['height'] + 1
310 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
315 tx_details = _("Transaction Details") +"\n\n" \
316 + "Transaction ID:\n" + tx_hash + "\n\n" \
317 + "Status: %d confirmations\n\n"%conf \
318 + "Date: %s\n\n"%time_str \
319 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
320 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
322 r = self.wallet.receipts.get(tx_hash)
324 tx_details += "\n_______________________________________" \
325 + '\n\nSigned URI: ' + r[2] \
326 + "\n\nSigned by: " + r[0] \
327 + '\n\nSignature: ' + r[1]
329 QMessageBox.information(self, 'Details', tx_details, 'OK')
332 def tx_label_clicked(self, item, column):
333 if column==2 and item.isSelected():
334 tx_hash = str(item.toolTip(0))
336 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
337 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
338 self.history_list.editItem( item, column )
339 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
342 def tx_label_changed(self, item, column):
346 tx_hash = str(item.toolTip(0))
347 tx = self.wallet.tx_history.get(tx_hash)
348 s = self.wallet.labels.get(tx_hash)
349 text = unicode( item.text(2) )
351 self.wallet.labels[tx_hash] = text
352 item.setForeground(2, QBrush(QColor('black')))
354 if s: self.wallet.labels.pop(tx_hash)
355 text = tx['default_label']
356 item.setText(2, text)
357 item.setForeground(2, QBrush(QColor('gray')))
360 def edit_label(self, is_recv):
361 l = self.receive_list if is_recv else self.contacts_list
362 c = 2 if is_recv else 1
363 item = l.currentItem()
364 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
365 l.editItem( item, c )
366 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
368 def address_label_clicked(self, item, column, l, column_addr, column_label):
369 if column==column_label and item.isSelected():
370 addr = unicode( item.text(column_addr) )
371 label = unicode( item.text(column_label) )
372 if label in self.wallet.aliases.keys():
374 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
375 l.editItem( item, column )
376 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
378 def address_label_changed(self, item, column, l, column_addr, column_label):
379 addr = unicode( item.text(column_addr) )
380 text = unicode( item.text(column_label) )
382 if text not in self.wallet.aliases.keys():
383 self.wallet.labels[addr] = text
385 print "error: this is one of your aliases"
386 label = self.wallet.labels.get(addr,'')
387 item.setText(column_label, QString(label))
389 s = self.wallet.labels.get(addr)
390 if s: self.wallet.labels.pop(addr)
392 self.update_history_tab()
393 self.update_completions()
395 def update_history_tab(self):
396 self.history_list.clear()
398 for tx in self.wallet.get_tx_history():
399 tx_hash = tx['tx_hash']
401 conf = self.wallet.blocks - tx['height'] + 1
402 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
403 icon = QIcon(":icons/confirmed.png")
407 icon = QIcon(":icons/unconfirmed.png")
410 label = self.wallet.labels.get(tx_hash)
411 is_default_label = (label == '') or (label is None)
412 if is_default_label: label = tx['default_label']
414 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
415 item.setFont(2, QFont(MONOSPACE_FONT))
416 item.setFont(3, QFont(MONOSPACE_FONT))
417 item.setFont(4, QFont(MONOSPACE_FONT))
418 item.setToolTip(0, tx_hash)
420 item.setForeground(2, QBrush(QColor('grey')))
422 item.setIcon(0, icon)
423 self.history_list.insertTopLevelItem(0,item)
425 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
428 def create_send_tab(self):
433 grid.setColumnMinimumWidth(3,300)
434 grid.setColumnStretch(5,1)
436 self.payto_e = QLineEdit()
437 grid.addWidget(QLabel(_('Pay to')), 1, 0)
438 grid.addWidget(self.payto_e, 1, 1, 1, 3)
439 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)
441 completer = QCompleter()
442 completer.setCaseSensitivity(False)
443 self.payto_e.setCompleter(completer)
444 completer.setModel(self.completions)
446 self.message_e = QLineEdit()
447 grid.addWidget(QLabel(_('Description')), 2, 0)
448 grid.addWidget(self.message_e, 2, 1, 1, 3)
449 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)
451 self.amount_e = QLineEdit()
452 grid.addWidget(QLabel(_('Amount')), 3, 0)
453 grid.addWidget(self.amount_e, 3, 1, 1, 2)
454 grid.addWidget(HelpButton(
455 _('Amount to be sent.') + '\n\n' \
456 + _('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)
458 self.fee_e = QLineEdit()
459 grid.addWidget(QLabel(_('Fee')), 4, 0)
460 grid.addWidget(self.fee_e, 4, 1, 1, 2)
461 grid.addWidget(HelpButton(
462 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
463 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
464 + _('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)
466 b = EnterButton(_("Send"), self.do_send)
467 grid.addWidget(b, 6, 1)
469 b = EnterButton(_("Clear"),self.do_clear)
470 grid.addWidget(b, 6, 2)
472 self.payto_sig = QLabel('')
473 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
475 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
476 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
486 def entry_changed( is_fee ):
487 self.funds_error = False
488 amount = numbify(self.amount_e)
489 fee = numbify(self.fee_e)
490 if not is_fee: fee = None
493 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
495 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
498 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
501 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
502 self.funds_error = True
503 self.amount_e.setPalette(palette)
504 self.fee_e.setPalette(palette)
506 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
507 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
512 def update_completions(self):
514 for addr,label in self.wallet.labels.items():
515 if addr in self.wallet.addressbook:
516 l.append( label + ' <' + addr + '>')
517 l = l + self.wallet.aliases.keys()
519 self.completions.setStringList(l)
525 label = unicode( self.message_e.text() )
526 r = unicode( self.payto_e.text() )
530 m1 = re.match(ALIAS_REGEXP, r)
531 # label or alias, with address in brackets
532 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
535 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
539 to_address = m2.group(2)
543 if not self.wallet.is_valid(to_address):
544 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
548 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
550 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
553 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
555 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
558 if self.wallet.use_encryption:
559 password = self.password_dialog()
566 tx = self.wallet.mktx( to_address, amount, label, password, fee)
567 except BaseException, e:
568 self.show_message(str(e))
571 status, msg = self.wallet.sendtx( tx )
573 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
575 self.update_contacts_tab()
577 QMessageBox.warning(self, _('Error'), msg, _('OK'))
580 def set_url(self, url):
581 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
582 self.tabs.setCurrentIndex(1)
583 label = self.wallet.labels.get(payto)
584 m_addr = label + ' <'+ payto+'>' if label else payto
585 self.payto_e.setText(m_addr)
587 self.message_e.setText(message)
588 self.amount_e.setText(amount)
590 self.set_frozen(self.payto_e,True)
591 self.set_frozen(self.amount_e,True)
592 self.set_frozen(self.message_e,True)
593 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
595 self.payto_sig.setVisible(False)
598 self.payto_sig.setVisible(False)
599 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
601 self.set_frozen(e,False)
603 def set_frozen(self,entry,frozen):
605 entry.setReadOnly(True)
606 entry.setFrame(False)
608 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
609 entry.setPalette(palette)
611 entry.setReadOnly(False)
614 palette.setColor(entry.backgroundRole(), QColor('white'))
615 entry.setPalette(palette)
618 def toggle_freeze(self,addr):
620 if addr in self.wallet.frozen_addresses:
621 self.wallet.unfreeze(addr)
623 self.wallet.freeze(addr)
624 self.update_receive_tab()
626 def toggle_priority(self,addr):
628 if addr in self.wallet.prioritized_addresses:
629 self.wallet.unprioritize(addr)
631 self.wallet.prioritize(addr)
632 self.update_receive_tab()
635 def create_list_tab(self, headers):
636 "generic tab creation method"
637 l = MyTreeWidget(self)
638 l.setColumnCount( len(headers) )
639 l.setHeaderLabels( headers )
649 vbox.addWidget(buttons)
654 buttons.setLayout(hbox)
659 def create_receive_tab(self):
660 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
661 l.setContextMenuPolicy(Qt.CustomContextMenu)
662 l.customContextMenuRequested.connect(self.create_receive_menu)
663 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
664 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
665 self.receive_list = l
666 self.receive_buttons_hbox = hbox
670 def create_contacts_tab(self):
671 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
672 l.setContextMenuPolicy(Qt.CustomContextMenu)
673 l.customContextMenuRequested.connect(self.create_contact_menu)
674 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
675 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
676 self.contacts_list = l
677 self.contacts_buttons_hbox = hbox
678 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
683 def create_receive_menu(self, position):
684 # fixme: this function apparently has a side effect.
685 # if it is not called the menu pops up several times
686 #self.receive_list.selectedIndexes()
688 item = self.receive_list.itemAt(position)
690 addr = unicode(item.text(1))
692 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
693 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
694 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
695 if self.wallet.expert_mode:
696 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
697 menu.addAction(t, lambda: self.toggle_freeze(addr))
698 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
699 menu.addAction(t, lambda: self.toggle_priority(addr))
700 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
703 def payto(self, x, is_alias):
710 label = self.wallet.labels.get(addr)
711 m_addr = label + ' <' + addr + '>' if label else addr
712 self.tabs.setCurrentIndex(1)
713 self.payto_e.setText(m_addr)
714 self.amount_e.setFocus()
716 def delete_contact(self, x, is_alias):
717 if self.question("Do you want to remove %s from your list of contacts?"%x):
718 if not is_alias and x in self.wallet.addressbook:
719 self.wallet.addressbook.remove(x)
720 if x in self.wallet.labels.keys():
721 self.wallet.labels.pop(x)
722 elif is_alias and x in self.wallet.aliases:
723 self.wallet.aliases.pop(x)
724 self.update_history_tab()
725 self.update_contacts_tab()
726 self.update_completions()
728 def create_contact_menu(self, position):
729 # fixme: this function apparently has a side effect.
730 # if it is not called the menu pops up several times
731 #self.contacts_list.selectedIndexes()
733 item = self.contacts_list.itemAt(position)
735 addr = unicode(item.text(0))
736 label = unicode(item.text(1))
737 is_alias = label in self.wallet.aliases.keys()
738 x = label if is_alias else addr
740 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
741 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
742 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
744 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
746 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
747 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
748 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
751 def update_receive_tab(self):
752 l = self.receive_list
754 l.setColumnHidden(0,not self.wallet.expert_mode)
755 l.setColumnHidden(3,not self.wallet.expert_mode)
756 l.setColumnHidden(4,not self.wallet.expert_mode)
757 l.setColumnWidth(0, 50)
758 l.setColumnWidth(1, 310)
759 l.setColumnWidth(2, 250)
760 l.setColumnWidth(3, 130)
761 l.setColumnWidth(4, 10)
765 for address in self.wallet.all_addresses():
767 if self.wallet.is_change(address) and not self.wallet.expert_mode:
770 label = self.wallet.labels.get(address,'')
772 h = self.wallet.history.get(address,[])
774 if not item['is_input'] : n=n+1
778 if address in self.wallet.addresses:
780 if gap > self.wallet.gap_limit:
783 if address in self.wallet.addresses:
786 c, u = self.wallet.get_addr_balance(address)
787 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
788 flags = self.wallet.get_address_flags(address)
789 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
791 item.setFont(0, QFont(MONOSPACE_FONT))
792 item.setFont(1, QFont(MONOSPACE_FONT))
793 item.setFont(3, QFont(MONOSPACE_FONT))
794 if address in self.wallet.frozen_addresses:
795 item.setBackgroundColor(1, QColor('lightblue'))
796 elif address in self.wallet.prioritized_addresses:
797 item.setBackgroundColor(1, QColor('lightgreen'))
798 if is_red and address in self.wallet.addresses:
799 item.setBackgroundColor(1, QColor('red'))
800 l.addTopLevelItem(item)
802 # we use column 1 because column 0 may be hidden
803 l.setCurrentItem(l.topLevelItem(0),1)
805 def show_contact_details(self, m):
806 a = self.wallet.aliases.get(m)
808 if a[0] in self.wallet.authorities.keys():
809 s = self.wallet.authorities.get(a[0])
812 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
813 QMessageBox.information(self, 'Alias', msg, 'OK')
815 def update_contacts_tab(self):
817 l = self.contacts_list
819 l.setColumnHidden(2, not self.wallet.expert_mode)
820 l.setColumnWidth(0, 350)
821 l.setColumnWidth(1, 330)
822 l.setColumnWidth(2, 100)
825 for alias, v in self.wallet.aliases.items():
827 alias_targets.append(target)
828 item = QTreeWidgetItem( [ target, alias, '-'] )
829 item.setBackgroundColor(0, QColor('lightgray'))
830 l.addTopLevelItem(item)
832 for address in self.wallet.addressbook:
833 if address in alias_targets: continue
834 label = self.wallet.labels.get(address,'')
836 for item in self.wallet.tx_history.values():
837 if address in item['outputs'] : n=n+1
839 item = QTreeWidgetItem( [ address, label, tx] )
840 item.setFont(0, QFont(MONOSPACE_FONT))
841 l.addTopLevelItem(item)
843 l.setCurrentItem(l.topLevelItem(0))
845 def create_wall_tab(self):
846 self.textbox = textbox = QTextEdit(self)
847 textbox.setFont(QFont(MONOSPACE_FONT))
848 textbox.setReadOnly(True)
851 def create_status_bar(self):
853 sb.setFixedHeight(35)
855 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
856 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
858 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
859 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
860 sb.addPermanentWidget( self.status_button )
861 self.setStatusBar(sb)
863 def new_contact_dialog(self):
864 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
865 address = unicode(text)
867 if self.wallet.is_valid(address):
868 self.wallet.addressbook.append(address)
870 self.update_contacts_tab()
871 self.update_history_tab()
872 self.update_completions()
874 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
877 def show_seed_dialog(wallet, parent=None):
880 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
883 if wallet.use_encryption:
884 password = parent.password_dialog()
885 if not password: return
890 seed = wallet.pw_decode( wallet.seed, password)
892 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
895 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
896 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
897 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
898 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
902 d.setWindowTitle(_("Seed"))
903 d.setMinimumSize(400, 270)
907 vbox2 = QVBoxLayout()
909 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
912 hbox.addLayout(vbox2)
913 hbox.addWidget(QLabel(msg))
925 b = QPushButton(_("Copy to Clipboard"))
926 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
928 b = QPushButton(_("View as QR Code"))
929 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
932 b = QPushButton(_("OK"))
933 b.clicked.connect(d.accept)
940 def show_seed_qrcode(seed):
944 d.setWindowTitle(_("Seed"))
945 d.setMinimumSize(270, 300)
947 vbox.addWidget(QRCodeWidget(seed))
950 b = QPushButton(_("OK"))
952 b.clicked.connect(d.accept)
959 def show_address_qrcode(self,address):
960 if not address: return
963 d.setWindowTitle(address)
964 d.setMinimumSize(270, 350)
966 qrw = QRCodeWidget(address)
970 amount_e = QLineEdit()
971 hbox.addWidget(QLabel(_('Amount')))
972 hbox.addWidget(amount_e)
975 #hbox = QHBoxLayout()
976 #label_e = QLineEdit()
977 #hbox.addWidget(QLabel('Label'))
978 #hbox.addWidget(label_e)
979 #vbox.addLayout(hbox)
981 def amount_changed():
982 amount = numbify(amount_e)
983 #label = str( label_e.getText() )
984 if amount is not None:
985 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
987 qrw.set_addr( address )
991 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
992 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
994 amount_e.textChanged.connect( amount_changed )
998 b = QPushButton(_("Save"))
999 b.clicked.connect(do_save)
1001 b = QPushButton(_("Close"))
1003 b.clicked.connect(d.accept)
1005 vbox.addLayout(hbox)
1009 def question(self, msg):
1010 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1012 def show_message(self, msg):
1013 QMessageBox.information(self, _('Message'), msg, _('OK'))
1015 def password_dialog(self ):
1022 vbox = QVBoxLayout()
1023 msg = _('Please enter your password')
1024 vbox.addWidget(QLabel(msg))
1026 grid = QGridLayout()
1028 grid.addWidget(QLabel(_('Password')), 1, 0)
1029 grid.addWidget(pw, 1, 1)
1030 vbox.addLayout(grid)
1032 vbox.addLayout(ok_cancel_buttons(d))
1035 if not d.exec_(): return
1036 return unicode(pw.text())
1043 def change_password_dialog( wallet, parent=None ):
1046 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1054 new_pw = QLineEdit()
1055 new_pw.setEchoMode(2)
1056 conf_pw = QLineEdit()
1057 conf_pw.setEchoMode(2)
1059 vbox = QVBoxLayout()
1061 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')
1063 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1064 vbox.addWidget(QLabel(msg))
1066 grid = QGridLayout()
1069 if wallet.use_encryption:
1070 grid.addWidget(QLabel(_('Password')), 1, 0)
1071 grid.addWidget(pw, 1, 1)
1073 grid.addWidget(QLabel(_('New Password')), 2, 0)
1074 grid.addWidget(new_pw, 2, 1)
1076 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1077 grid.addWidget(conf_pw, 3, 1)
1078 vbox.addLayout(grid)
1080 vbox.addLayout(ok_cancel_buttons(d))
1083 if not d.exec_(): return
1085 password = unicode(pw.text()) if wallet.use_encryption else None
1086 new_password = unicode(new_pw.text())
1087 new_password2 = unicode(conf_pw.text())
1090 seed = wallet.pw_decode( wallet.seed, password)
1092 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1095 if new_password != new_password2:
1096 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1099 wallet.update_password(seed, password, new_password)
1102 def seed_dialog(wallet, parent=None):
1106 vbox = QVBoxLayout()
1107 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1108 vbox.addWidget(QLabel(msg))
1110 grid = QGridLayout()
1113 seed_e = QLineEdit()
1114 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1115 grid.addWidget(seed_e, 1, 1)
1119 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1120 grid.addWidget(gap_e, 2, 1)
1121 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1122 vbox.addLayout(grid)
1124 vbox.addLayout(ok_cancel_buttons(d))
1127 if not d.exec_(): return
1130 gap = int(unicode(gap_e.text()))
1132 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1136 seed = unicode(seed_e.text())
1139 print "not hex, trying decode"
1141 seed = mnemonic.mn_decode( seed.split(' ') )
1143 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1146 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1149 wallet.seed = str(seed)
1150 #print repr(wallet.seed)
1151 wallet.gap_limit = gap
1155 def set_expert_mode(self, b):
1156 self.wallet.expert_mode = b
1158 self.update_receive_tab()
1159 self.update_contacts_tab()
1160 # if self.wallet.seed:
1161 # self.nochange_cb.setHidden(not self.wallet.expert_mode)
1164 def settings_dialog(self):
1167 vbox = QVBoxLayout()
1168 msg = _('Here are the settings of your wallet.') + '\n'\
1169 + _('For more explanations, click on the help buttons next to each field.')
1172 label.setFixedWidth(250)
1173 label.setWordWrap(True)
1174 label.setAlignment(Qt.AlignJustify)
1175 vbox.addWidget(label)
1177 grid = QGridLayout()
1179 vbox.addLayout(grid)
1182 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1183 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1184 grid.addWidget(fee_e, 2, 1)
1185 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1186 + _('Recommended value') + ': 0.001'
1187 grid.addWidget(HelpButton(msg), 2, 2)
1188 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1191 nz_e.setText("%d"% self.wallet.num_zeros)
1192 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1193 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1194 grid.addWidget(HelpButton(msg), 3, 2)
1195 grid.addWidget(nz_e, 3, 1)
1196 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1198 cb = QCheckBox(_('Expert mode'))
1199 grid.addWidget(cb, 4, 0)
1200 cb.setChecked(self.wallet.expert_mode)
1202 if self.wallet.expert_mode:
1204 usechange_cb = QCheckBox(_('Use change addresses'))
1205 grid.addWidget(usechange_cb, 5, 0)
1206 usechange_cb.setChecked(self.wallet.use_change)
1207 grid.addWidget(HelpButton(_('Using a change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1209 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1210 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1211 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1212 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1213 + _('Warning') + ': ' \
1214 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1215 + _('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'
1217 gap_e.setText("%d"% self.wallet.gap_limit)
1218 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1219 grid.addWidget(gap_e, 6, 1)
1220 grid.addWidget(HelpButton(msg), 6, 2)
1221 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1224 vbox.addLayout(ok_cancel_buttons(d))
1228 if not d.exec_(): return
1230 fee = unicode(fee_e.text())
1232 fee = int( 100000000 * Decimal(fee) )
1234 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1237 if self.wallet.fee != fee:
1238 self.wallet.fee = fee
1241 nz = unicode(nz_e.text())
1246 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1249 if self.wallet.num_zeros != nz:
1250 self.wallet.num_zeros = nz
1251 self.update_history_tab()
1252 self.update_receive_tab()
1255 if self.wallet.expert_mode:
1257 self.wallet.use_change = usechange_cb.isChecked()
1260 n = int(gap_e.text())
1262 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1264 if self.wallet.gap_limit != n:
1265 r = self.wallet.change_gap_limit(n)
1267 self.update_receive_tab()
1269 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1271 self.set_expert_mode(cb.isChecked())
1275 def network_dialog(wallet, parent=None):
1276 interface = wallet.interface
1278 if interface.is_connected:
1279 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1281 status = _("Not connected")
1282 server = wallet.server
1285 status = _("Please choose a server.")
1286 server = random.choice( DEFAULT_SERVERS )
1288 if not wallet.interface.servers:
1290 for x in DEFAULT_SERVERS:
1291 h,port,protocol = x.split(':')
1292 servers_list.append( (h,[(protocol,port)] ) )
1294 servers_list = wallet.interface.servers
1297 for item in servers_list:
1301 protocol, port = item2
1307 d.setWindowTitle(_('Server'))
1308 d.setMinimumSize(375, 20)
1310 vbox = QVBoxLayout()
1313 hbox = QHBoxLayout()
1315 l.setPixmap(QPixmap(":icons/network.png"))
1317 hbox.addWidget(QLabel(status))
1319 vbox.addLayout(hbox)
1321 hbox = QHBoxLayout()
1322 host_line = QLineEdit()
1323 host_line.setText(server)
1324 hbox.addWidget(QLabel(_('Connect to') + ':'))
1325 hbox.addWidget(host_line)
1326 vbox.addLayout(hbox)
1328 hbox = QHBoxLayout()
1330 buttonGroup = QGroupBox(_("Protocol"))
1331 radio1 = QRadioButton("tcp", buttonGroup)
1332 radio2 = QRadioButton("http", buttonGroup)
1335 return unicode(host_line.text()).split(':')
1337 def set_button(protocol):
1339 radio1.setChecked(1)
1340 elif protocol == 'h':
1341 radio2.setChecked(1)
1343 def set_protocol(protocol):
1344 host = current_line()[0]
1346 if protocol not in pp.keys():
1347 protocol = pp.keys()[0]
1348 set_button(protocol)
1350 host_line.setText( host + ':' + port + ':' + protocol)
1352 radio1.clicked.connect(lambda x: set_protocol('t') )
1353 radio2.clicked.connect(lambda x: set_protocol('h') )
1355 set_button(current_line()[2])
1357 hbox.addWidget(QLabel(_('Protocol')+':'))
1358 hbox.addWidget(radio1)
1359 hbox.addWidget(radio2)
1361 vbox.addLayout(hbox)
1363 if wallet.interface.servers:
1364 label = _('Active Servers')
1366 label = _('Default Servers')
1368 servers_list_widget = QTreeWidget(parent)
1369 servers_list_widget.setHeaderLabels( [ label ] )
1370 servers_list_widget.setMaximumHeight(150)
1371 for host in plist.keys():
1372 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1375 host = unicode(x.text(0))
1377 if 't' in pp.keys():
1380 protocol = pp.keys()[0]
1382 host_line.setText( host + ':' + port + ':' + protocol)
1383 set_button(protocol)
1385 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1386 vbox.addWidget(servers_list_widget)
1388 vbox.addLayout(ok_cancel_buttons(d))
1391 if not d.exec_(): return
1392 server = unicode( host_line.text() )
1395 wallet.set_server(server)
1397 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1407 class ElectrumGui():
1409 def __init__(self, wallet):
1410 self.wallet = wallet
1411 self.app = QApplication(sys.argv)
1413 def waiting_dialog(self):
1419 w.setWindowTitle('Electrum')
1421 vbox = QVBoxLayout()
1426 if self.wallet.up_to_date:
1429 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1430 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1432 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1433 self.wallet.interface.poke()
1438 def restore_or_create(self):
1440 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1441 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1442 if r==2: return False
1444 is_recovery = (r==1)
1445 wallet = self.wallet
1446 # ask for the server.
1447 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1450 wallet.new_seed(None)
1451 wallet.init_mpk( wallet.seed )
1452 wallet.up_to_date_event.clear()
1453 wallet.up_to_date = False
1454 self.waiting_dialog()
1455 # run a dialog indicating the seed, ask the user to remember it
1456 ElectrumWindow.show_seed_dialog(wallet)
1458 ElectrumWindow.change_password_dialog(wallet)
1460 # ask for seed and gap.
1461 if not ElectrumWindow.seed_dialog( wallet ): return False
1462 wallet.init_mpk( wallet.seed )
1463 wallet.up_to_date_event.clear()
1464 wallet.up_to_date = False
1465 self.waiting_dialog()
1466 if wallet.is_found():
1467 # history and addressbook
1468 wallet.update_tx_history()
1469 wallet.fill_addressbook()
1470 print "recovery successful"
1473 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1481 w = ElectrumWindow(self.wallet)
1482 if url: w.set_url(url)