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 = 2 if self.wallet.seed else 1
216 tabs.setCurrentIndex (n)
217 tabs.setCurrentIndex (n+1)
218 tabs.setCurrentIndex (0)
221 def connect_slots(self, sender):
223 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
224 self.previous_payto_e=''
226 def check_recipient(self):
227 if self.payto_e.hasFocus():
229 r = unicode( self.payto_e.text() )
230 if r != self.previous_payto_e:
231 self.previous_payto_e = r
233 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
235 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
239 s = r + ' <' + to_address + '>'
240 self.payto_e.setText(s)
243 def update_callback(self):
244 self.emit(QtCore.SIGNAL('updatesignal'))
246 def update_wallet(self):
247 if self.wallet.interface and self.wallet.interface.is_connected:
248 if self.wallet.blocks == -1:
249 text = _( "Connecting..." )
250 icon = QIcon(":icons/status_disconnected.png")
251 elif self.wallet.blocks == 0:
252 text = _( "Server not ready" )
253 icon = QIcon(":icons/status_disconnected.png")
254 elif not self.wallet.up_to_date:
255 text = _( "Synchronizing..." )
256 icon = QIcon(":icons/status_waiting.png")
258 c, u = self.wallet.get_balance()
259 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
260 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
261 icon = QIcon(":icons/status_connected.png")
263 text = _( "Not connected" )
264 icon = QIcon(":icons/status_disconnected.png")
267 text = _( "Not enough funds" )
269 self.statusBar().showMessage(text)
270 self.status_button.setIcon( icon )
272 if self.wallet.up_to_date:
273 self.textbox.setText( self.wallet.banner )
274 self.update_history_tab()
275 self.update_receive_tab()
276 self.update_contacts_tab()
277 self.update_completions()
280 def create_history_tab(self):
281 self.history_list = l = MyTreeWidget(self)
283 l.setColumnWidth(0, 40)
284 l.setColumnWidth(1, 140)
285 l.setColumnWidth(2, 350)
286 l.setColumnWidth(3, 140)
287 l.setColumnWidth(4, 140)
288 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
289 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
290 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
291 l.setContextMenuPolicy(Qt.CustomContextMenu)
292 l.customContextMenuRequested.connect(self.create_history_menu)
295 def create_history_menu(self, position):
296 self.history_list.selectedIndexes()
297 item = self.history_list.currentItem()
299 tx_hash = str(item.toolTip(0))
301 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
302 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
303 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
304 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
306 def tx_details(self, tx_hash):
307 tx = self.wallet.tx_history.get(tx_hash)
310 conf = self.wallet.blocks - tx['height'] + 1
311 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
316 tx_details = _("Transaction Details") +"\n\n" \
317 + "Transaction ID:\n" + tx_hash + "\n\n" \
318 + "Status: %d confirmations\n\n"%conf \
319 + "Date: %s\n\n"%time_str \
320 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
321 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
323 r = self.wallet.receipts.get(tx_hash)
325 tx_details += "\n_______________________________________" \
326 + '\n\nSigned URI: ' + r[2] \
327 + "\n\nSigned by: " + r[0] \
328 + '\n\nSignature: ' + r[1]
330 QMessageBox.information(self, 'Details', tx_details, 'OK')
333 def tx_label_clicked(self, item, column):
334 if column==2 and item.isSelected():
335 tx_hash = str(item.toolTip(0))
337 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
338 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
339 self.history_list.editItem( item, column )
340 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
343 def tx_label_changed(self, item, column):
347 tx_hash = str(item.toolTip(0))
348 tx = self.wallet.tx_history.get(tx_hash)
349 s = self.wallet.labels.get(tx_hash)
350 text = unicode( item.text(2) )
352 self.wallet.labels[tx_hash] = text
353 item.setForeground(2, QBrush(QColor('black')))
355 if s: self.wallet.labels.pop(tx_hash)
356 text = tx['default_label']
357 item.setText(2, text)
358 item.setForeground(2, QBrush(QColor('gray')))
361 def edit_label(self, is_recv):
362 l = self.receive_list if is_recv else self.contacts_list
363 c = 2 if is_recv else 1
364 item = l.currentItem()
365 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
366 l.editItem( item, c )
367 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
369 def address_label_clicked(self, item, column, l, column_addr, column_label):
370 if column==column_label and item.isSelected():
371 addr = unicode( item.text(column_addr) )
372 label = unicode( item.text(column_label) )
373 if label in self.wallet.aliases.keys():
375 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
376 l.editItem( item, column )
377 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
379 def address_label_changed(self, item, column, l, column_addr, column_label):
380 addr = unicode( item.text(column_addr) )
381 text = unicode( item.text(column_label) )
383 if text not in self.wallet.aliases.keys():
384 self.wallet.labels[addr] = text
386 print "error: this is one of your aliases"
387 label = self.wallet.labels.get(addr,'')
388 item.setText(column_label, QString(label))
390 s = self.wallet.labels.get(addr)
391 if s: self.wallet.labels.pop(addr)
393 self.update_history_tab()
394 self.update_completions()
396 def update_history_tab(self):
397 self.history_list.clear()
399 for tx in self.wallet.get_tx_history():
400 tx_hash = tx['tx_hash']
402 conf = self.wallet.blocks - tx['height'] + 1
403 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
404 icon = QIcon(":icons/confirmed.png")
408 icon = QIcon(":icons/unconfirmed.png")
411 label = self.wallet.labels.get(tx_hash)
412 is_default_label = (label == '') or (label is None)
413 if is_default_label: label = tx['default_label']
415 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
416 item.setFont(2, QFont(MONOSPACE_FONT))
417 item.setFont(3, QFont(MONOSPACE_FONT))
418 item.setFont(4, QFont(MONOSPACE_FONT))
419 item.setToolTip(0, tx_hash)
421 item.setForeground(2, QBrush(QColor('grey')))
423 item.setIcon(0, icon)
424 self.history_list.insertTopLevelItem(0,item)
426 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
429 def create_send_tab(self):
434 grid.setColumnMinimumWidth(3,300)
435 grid.setColumnStretch(5,1)
437 self.payto_e = QLineEdit()
438 grid.addWidget(QLabel(_('Pay to')), 1, 0)
439 grid.addWidget(self.payto_e, 1, 1, 1, 3)
440 grid.addWidget(HelpButton(_('Recipient of the funds.\n\nYou 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)
442 completer = QCompleter()
443 completer.setCaseSensitivity(False)
444 self.payto_e.setCompleter(completer)
445 completer.setModel(self.completions)
447 self.message_e = QLineEdit()
448 grid.addWidget(QLabel(_('Description')), 2, 0)
449 grid.addWidget(self.message_e, 2, 1, 1, 3)
450 grid.addWidget(HelpButton(_('Description of the transaction (not mandatory).\n\nThe 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)
452 self.amount_e = QLineEdit()
453 grid.addWidget(QLabel(_('Amount')), 3, 0)
454 grid.addWidget(self.amount_e, 3, 1, 1, 2)
455 grid.addWidget(HelpButton(_('Amount to be sent.\n\nThe 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)
457 self.fee_e = QLineEdit()
458 grid.addWidget(QLabel(_('Fee')), 4, 0)
459 grid.addWidget(self.fee_e, 4, 1, 1, 2)
460 grid.addWidget(HelpButton(_('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.\n\nThe amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.\n\nA suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.')), 4, 3)
462 self.nochange_cb = QCheckBox(_('Do not create change address'))
463 grid.addWidget(self.nochange_cb,5,1,1,4)
464 self.nochange_cb.setChecked(False)
465 self.nochange_cb.setHidden(not self.wallet.expert_mode)
467 b = EnterButton(_("Send"), self.do_send)
468 grid.addWidget(b, 6, 1)
470 b = EnterButton(_("Clear"),self.do_clear)
471 grid.addWidget(b, 6, 2)
473 self.payto_sig = QLabel('')
474 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
476 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
477 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
487 def entry_changed( is_fee ):
488 self.funds_error = False
489 amount = numbify(self.amount_e)
490 fee = numbify(self.fee_e)
491 if not is_fee: fee = None
494 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
496 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
499 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
502 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
503 self.funds_error = True
504 self.amount_e.setPalette(palette)
505 self.fee_e.setPalette(palette)
507 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
508 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
513 def update_completions(self):
515 for addr,label in self.wallet.labels.items():
516 if addr in self.wallet.addressbook:
517 l.append( label + ' <' + addr + '>')
518 l = l + self.wallet.aliases.keys()
520 self.completions.setStringList(l)
526 label = unicode( self.message_e.text() )
527 r = unicode( self.payto_e.text() )
531 m1 = re.match(ALIAS_REGEXP, r)
532 # label or alias, with address in brackets
533 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
536 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
540 to_address = m2.group(2)
544 if not self.wallet.is_valid(to_address):
545 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
549 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
551 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
554 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
556 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
559 if self.wallet.use_encryption:
560 password = self.password_dialog()
566 if self.nochange_cb.isChecked():
567 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
568 change_addr = inputs[0][0]
569 print "sending change to", change_addr
574 tx = self.wallet.mktx( to_address, amount, label, password, fee, change_addr )
575 except BaseException, e:
576 self.show_message(str(e))
579 status, msg = self.wallet.sendtx( tx )
581 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
583 self.update_contacts_tab()
585 QMessageBox.warning(self, _('Error'), msg, _('OK'))
588 def set_url(self, url):
589 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
590 self.tabs.setCurrentIndex(1)
591 label = self.wallet.labels.get(payto)
592 m_addr = label + ' <'+ payto+'>' if label else payto
593 self.payto_e.setText(m_addr)
595 self.message_e.setText(message)
596 self.amount_e.setText(amount)
598 self.set_frozen(self.payto_e,True)
599 self.set_frozen(self.amount_e,True)
600 self.set_frozen(self.message_e,True)
601 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
603 self.payto_sig.setVisible(False)
606 self.payto_sig.setVisible(False)
607 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
609 self.set_frozen(e,False)
611 def set_frozen(self,entry,frozen):
613 entry.setReadOnly(True)
614 entry.setFrame(False)
616 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
617 entry.setPalette(palette)
619 entry.setReadOnly(False)
622 palette.setColor(entry.backgroundRole(), QColor('white'))
623 entry.setPalette(palette)
626 def toggle_freeze(self,addr):
628 if addr in self.wallet.frozen_addresses:
629 self.wallet.unfreeze(addr)
631 self.wallet.freeze(addr)
632 self.update_receive_tab()
634 def toggle_priority(self,addr):
636 if addr in self.wallet.prioritized_addresses:
637 self.wallet.unprioritize(addr)
639 self.wallet.prioritize(addr)
640 self.update_receive_tab()
643 def create_list_tab(self, headers):
644 "generic tab creation method"
645 l = MyTreeWidget(self)
646 l.setColumnCount( len(headers) )
647 l.setHeaderLabels( headers )
657 vbox.addWidget(buttons)
662 buttons.setLayout(hbox)
667 def create_receive_tab(self):
668 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Received')])
669 l.setContextMenuPolicy(Qt.CustomContextMenu)
670 l.customContextMenuRequested.connect(self.create_receive_menu)
671 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
672 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
673 self.receive_list = l
674 self.receive_buttons_hbox = hbox
678 def create_contacts_tab(self):
679 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
680 l.setContextMenuPolicy(Qt.CustomContextMenu)
681 l.customContextMenuRequested.connect(self.create_contact_menu)
682 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
683 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
684 self.contacts_list = l
685 self.contacts_buttons_hbox = hbox
686 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
691 def create_receive_menu(self, position):
692 # fixme: this function apparently has a side effect.
693 # if it is not called the menu pops up several times
694 #self.receive_list.selectedIndexes()
696 item = self.receive_list.itemAt(position)
698 addr = unicode(item.text(1))
700 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
701 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
702 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
703 if self.wallet.expert_mode:
704 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
705 menu.addAction(t, lambda: self.toggle_freeze(addr))
706 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
707 menu.addAction(t, lambda: self.toggle_priority(addr))
708 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
711 def payto(self, x, is_alias):
718 label = self.wallet.labels.get(addr)
719 m_addr = label + ' <' + addr + '>' if label else addr
720 self.tabs.setCurrentIndex(1)
721 self.payto_e.setText(m_addr)
722 self.amount_e.setFocus()
724 def delete_contact(self, x, is_alias):
725 if self.question("Do you want to remove %s from your list of contacts?"%x):
726 if not is_alias and x in self.wallet.addressbook:
727 self.wallet.addressbook.remove(x)
728 if x in self.wallet.labels.keys():
729 self.wallet.labels.pop(x)
730 elif is_alias and x in self.wallet.aliases:
731 self.wallet.aliases.pop(x)
732 self.update_history_tab()
733 self.update_contacts_tab()
734 self.update_completions()
736 def create_contact_menu(self, position):
737 # fixme: this function apparently has a side effect.
738 # if it is not called the menu pops up several times
739 #self.contacts_list.selectedIndexes()
741 item = self.contacts_list.itemAt(position)
743 addr = unicode(item.text(0))
744 label = unicode(item.text(1))
745 is_alias = label in self.wallet.aliases.keys()
746 x = label if is_alias else addr
748 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
749 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
750 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
752 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
754 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
755 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
756 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
759 def update_receive_tab(self):
760 l = self.receive_list
762 l.setColumnHidden(0,not self.wallet.expert_mode)
763 l.setColumnHidden(3,not self.wallet.expert_mode)
764 #l.setColumnHidden(4,not self.wallet.expert_mode)
765 l.setColumnWidth(0, 50)
766 l.setColumnWidth(1, 310)
767 l.setColumnWidth(2, 250)
768 l.setColumnWidth(3, 130)
769 l.setColumnWidth(4, 10)
773 for address in self.wallet.all_addresses():
775 if self.wallet.is_change(address) and not self.wallet.expert_mode:
778 label = self.wallet.labels.get(address,'')
780 h = self.wallet.history.get(address,[])
782 if not item['is_input'] : n=n+1
786 if address in self.wallet.addresses:
788 if gap > self.wallet.gap_limit:
791 tx = "%d "%n + _('times')
792 if address in self.wallet.addresses:
795 c, u = self.wallet.get_addr_balance(address)
796 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
797 flags = self.wallet.get_address_flags(address)
798 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
800 item.setFont(0, QFont(MONOSPACE_FONT))
801 item.setFont(1, QFont(MONOSPACE_FONT))
802 item.setFont(3, QFont(MONOSPACE_FONT))
803 if address in self.wallet.frozen_addresses:
804 item.setBackgroundColor(1, QColor('lightblue'))
805 elif address in self.wallet.prioritized_addresses:
806 item.setBackgroundColor(1, QColor('lightgreen'))
807 if is_red and address in self.wallet.addresses:
808 item.setBackgroundColor(1, QColor('red'))
809 l.addTopLevelItem(item)
811 # we use column 1 because column 0 may be hidden
812 l.setCurrentItem(l.topLevelItem(0),1)
814 def show_contact_details(self, m):
815 a = self.wallet.aliases.get(m)
817 if a[0] in self.wallet.authorities.keys():
818 s = self.wallet.authorities.get(a[0])
821 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
822 QMessageBox.information(self, 'Alias', msg, 'OK')
824 def update_contacts_tab(self):
826 l = self.contacts_list
828 l.setColumnHidden(2, not self.wallet.expert_mode)
829 l.setColumnWidth(0, 350)
830 l.setColumnWidth(1, 330)
831 l.setColumnWidth(2, 100)
834 for alias, v in self.wallet.aliases.items():
836 alias_targets.append(target)
837 item = QTreeWidgetItem( [ target, alias, '-'] )
838 item.setBackgroundColor(0, QColor('lightgray'))
839 l.addTopLevelItem(item)
841 for address in self.wallet.addressbook:
842 if address in alias_targets: continue
843 label = self.wallet.labels.get(address,'')
845 for item in self.wallet.tx_history.values():
846 if address in item['outputs'] : n=n+1
847 tx = "None" if n==0 else "%d"%n
848 item = QTreeWidgetItem( [ address, label, tx] )
849 item.setFont(0, QFont(MONOSPACE_FONT))
850 l.addTopLevelItem(item)
852 l.setCurrentItem(l.topLevelItem(0))
854 def create_wall_tab(self):
855 self.textbox = textbox = QTextEdit(self)
856 textbox.setFont(QFont(MONOSPACE_FONT))
857 textbox.setReadOnly(True)
860 def create_status_bar(self):
862 sb.setFixedHeight(35)
864 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
865 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
867 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
868 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
869 sb.addPermanentWidget( self.status_button )
870 self.setStatusBar(sb)
872 def new_contact_dialog(self):
873 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
874 address = unicode(text)
876 if self.wallet.is_valid(address):
877 self.wallet.addressbook.append(address)
879 self.update_contacts_tab()
880 self.update_history_tab()
881 self.update_completions()
883 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
886 def show_seed_dialog(wallet, parent=None):
889 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
892 if wallet.use_encryption:
893 password = parent.password_dialog()
894 if not password: return
899 seed = wallet.pw_decode( wallet.seed, password)
901 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
904 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
905 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
906 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
907 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
911 d.setWindowTitle(_("Seed"))
912 d.setMinimumSize(400, 270)
916 vbox2 = QVBoxLayout()
918 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
921 hbox.addLayout(vbox2)
922 hbox.addWidget(QLabel(msg))
934 b = QPushButton(_("Copy to Clipboard"))
935 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
937 b = QPushButton(_("View as QR Code"))
938 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
941 b = QPushButton(_("OK"))
942 b.clicked.connect(d.accept)
949 def show_seed_qrcode(seed):
953 d.setWindowTitle(_("Seed"))
954 d.setMinimumSize(270, 300)
956 vbox.addWidget(QRCodeWidget(seed))
959 b = QPushButton(_("OK"))
961 b.clicked.connect(d.accept)
968 def show_address_qrcode(self,address):
969 if not address: return
972 d.setWindowTitle(address)
973 d.setMinimumSize(270, 350)
975 qrw = QRCodeWidget(address)
979 amount_e = QLineEdit()
980 hbox.addWidget(QLabel(_('Amount')))
981 hbox.addWidget(amount_e)
984 #hbox = QHBoxLayout()
985 #label_e = QLineEdit()
986 #hbox.addWidget(QLabel('Label'))
987 #hbox.addWidget(label_e)
988 #vbox.addLayout(hbox)
990 def amount_changed():
991 amount = numbify(amount_e)
992 #label = str( label_e.getText() )
993 if amount is not None:
994 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
996 qrw.set_addr( address )
1000 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
1001 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
1003 amount_e.textChanged.connect( amount_changed )
1005 hbox = QHBoxLayout()
1007 b = QPushButton(_("Save"))
1008 b.clicked.connect(do_save)
1010 b = QPushButton(_("Close"))
1012 b.clicked.connect(d.accept)
1014 vbox.addLayout(hbox)
1018 def question(self, msg):
1019 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1021 def show_message(self, msg):
1022 QMessageBox.information(self, _('Message'), msg, _('OK'))
1024 def password_dialog(self ):
1031 vbox = QVBoxLayout()
1032 msg = _('Please enter your password')
1033 vbox.addWidget(QLabel(msg))
1035 grid = QGridLayout()
1037 grid.addWidget(QLabel(_('Password')), 1, 0)
1038 grid.addWidget(pw, 1, 1)
1039 vbox.addLayout(grid)
1041 vbox.addLayout(ok_cancel_buttons(d))
1044 if not d.exec_(): return
1045 return unicode(pw.text())
1052 def change_password_dialog( wallet, parent=None ):
1055 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1063 new_pw = QLineEdit()
1064 new_pw.setEchoMode(2)
1065 conf_pw = QLineEdit()
1066 conf_pw.setEchoMode(2)
1068 vbox = QVBoxLayout()
1070 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')
1072 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1073 vbox.addWidget(QLabel(msg))
1075 grid = QGridLayout()
1078 if wallet.use_encryption:
1079 grid.addWidget(QLabel(_('Password')), 1, 0)
1080 grid.addWidget(pw, 1, 1)
1082 grid.addWidget(QLabel(_('New Password')), 2, 0)
1083 grid.addWidget(new_pw, 2, 1)
1085 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1086 grid.addWidget(conf_pw, 3, 1)
1087 vbox.addLayout(grid)
1089 vbox.addLayout(ok_cancel_buttons(d))
1092 if not d.exec_(): return
1094 password = unicode(pw.text()) if wallet.use_encryption else None
1095 new_password = unicode(new_pw.text())
1096 new_password2 = unicode(conf_pw.text())
1099 seed = wallet.pw_decode( wallet.seed, password)
1101 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1104 if new_password != new_password2:
1105 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1108 wallet.update_password(seed, password, new_password)
1111 def seed_dialog(wallet, parent=None):
1115 vbox = QVBoxLayout()
1116 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1117 vbox.addWidget(QLabel(msg))
1119 grid = QGridLayout()
1122 seed_e = QLineEdit()
1123 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1124 grid.addWidget(seed_e, 1, 1)
1128 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1129 grid.addWidget(gap_e, 2, 1)
1130 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1131 vbox.addLayout(grid)
1133 vbox.addLayout(ok_cancel_buttons(d))
1136 if not d.exec_(): return
1139 gap = int(unicode(gap_e.text()))
1141 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1145 seed = unicode(seed_e.text())
1148 print "not hex, trying decode"
1150 seed = mnemonic.mn_decode( seed.split(' ') )
1152 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1155 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1158 wallet.seed = str(seed)
1159 #print repr(wallet.seed)
1160 wallet.gap_limit = gap
1164 def set_expert_mode(self, b):
1165 self.wallet.expert_mode = b
1167 self.update_receive_tab()
1168 self.update_contacts_tab()
1169 if self.wallet.seed:
1170 self.nochange_cb.setHidden(not self.wallet.expert_mode)
1173 def settings_dialog(self):
1176 vbox = QVBoxLayout()
1177 msg = _('Here are the settings of your wallet.') + '\n'\
1178 + _('For more explanations, click on the help buttons next to each field.')
1181 label.setFixedWidth(250)
1182 label.setWordWrap(True)
1183 label.setAlignment(Qt.AlignJustify)
1184 vbox.addWidget(label)
1186 grid = QGridLayout()
1188 vbox.addLayout(grid)
1191 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1192 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1193 grid.addWidget(fee_e, 2, 1)
1194 grid.addWidget(HelpButton(_('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee. Recommended value: 0.001')), 2, 2)
1195 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1198 nz_e.setText("%d"% self.wallet.num_zeros)
1199 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1200 grid.addWidget(HelpButton(_('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')), 3, 2)
1201 grid.addWidget(nz_e, 3, 1)
1202 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1204 if self.wallet.expert_mode:
1205 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1206 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1207 + _('Your current gap limit is: ') + '%d'%self.wallet.gap_limit + '\n' \
1208 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1209 + _('Warning:') + ' ' \
1210 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1211 + _('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'
1213 gap_e.setText("%d"% self.wallet.gap_limit)
1214 grid.addWidget(QLabel(_('Gap limit')), 4, 0)
1215 grid.addWidget(gap_e, 4, 1)
1216 grid.addWidget(HelpButton(msg), 4, 2)
1217 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1219 cb = QCheckBox(_('Expert mode'))
1220 grid.addWidget(cb, 5, 0)
1221 cb.setChecked(self.wallet.expert_mode)
1223 vbox.addLayout(ok_cancel_buttons(d))
1227 if not d.exec_(): return
1229 fee = unicode(fee_e.text())
1231 fee = int( 100000000 * Decimal(fee) )
1233 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1236 if self.wallet.fee != fee:
1237 self.wallet.fee = fee
1240 nz = unicode(nz_e.text())
1245 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1248 if self.wallet.num_zeros != nz:
1249 self.wallet.num_zeros = nz
1250 self.update_history_tab()
1251 self.update_receive_tab()
1254 if self.wallet.expert_mode:
1256 n = int(gap_e.text())
1258 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1260 if self.wallet.gap_limit != n:
1261 r = self.wallet.change_gap_limit(n)
1263 self.update_receive_tab()
1265 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1267 self.set_expert_mode(cb.isChecked())
1271 def network_dialog(wallet, parent=None):
1272 interface = wallet.interface
1274 if interface.is_connected:
1275 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1277 status = _("Not connected")
1278 server = wallet.server
1281 status = _("Please choose a server.")
1282 server = random.choice( DEFAULT_SERVERS )
1284 if not wallet.interface.servers:
1286 for x in DEFAULT_SERVERS:
1287 h,port,protocol = x.split(':')
1288 servers_list.append( (h,[(protocol,port)] ) )
1290 servers_list = wallet.interface.servers
1293 for item in servers_list:
1297 protocol, port = item2
1303 d.setWindowTitle(_('Server'))
1304 d.setMinimumSize(375, 20)
1306 vbox = QVBoxLayout()
1309 hbox = QHBoxLayout()
1311 l.setPixmap(QPixmap(":icons/network.png"))
1313 hbox.addWidget(QLabel(status))
1315 vbox.addLayout(hbox)
1317 hbox = QHBoxLayout()
1318 host_line = QLineEdit()
1319 host_line.setText(server)
1320 hbox.addWidget(QLabel(_('Connect to') + ':'))
1321 hbox.addWidget(host_line)
1322 vbox.addLayout(hbox)
1324 hbox = QHBoxLayout()
1326 buttonGroup = QGroupBox(_("Protocol"))
1327 radio1 = QRadioButton("tcp", buttonGroup)
1328 radio2 = QRadioButton("http", buttonGroup)
1331 return unicode(host_line.text()).split(':')
1333 def set_button(protocol):
1335 radio1.setChecked(1)
1336 elif protocol == 'h':
1337 radio2.setChecked(1)
1339 def set_protocol(protocol):
1340 host = current_line()[0]
1342 if protocol not in pp.keys():
1343 protocol = pp.keys()[0]
1344 set_button(protocol)
1346 host_line.setText( host + ':' + port + ':' + protocol)
1348 radio1.clicked.connect(lambda x: set_protocol('t') )
1349 radio2.clicked.connect(lambda x: set_protocol('h') )
1351 set_button(current_line()[2])
1353 hbox.addWidget(QLabel(_('Protocol')+':'))
1354 hbox.addWidget(radio1)
1355 hbox.addWidget(radio2)
1357 vbox.addLayout(hbox)
1359 if wallet.interface.servers:
1360 label = _('Active Servers')
1362 label = _('Default Servers')
1364 servers_list_widget = QTreeWidget(parent)
1365 servers_list_widget.setHeaderLabels( [ label ] )
1366 servers_list_widget.setMaximumHeight(150)
1367 for host in plist.keys():
1368 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1371 host = unicode(x.text(0))
1373 if 't' in pp.keys():
1376 protocol = pp.keys()[0]
1378 host_line.setText( host + ':' + port + ':' + protocol)
1379 set_button(protocol)
1381 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1382 vbox.addWidget(servers_list_widget)
1384 vbox.addLayout(ok_cancel_buttons(d))
1387 if not d.exec_(): return
1388 server = unicode( host_line.text() )
1391 wallet.set_server(server)
1393 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1403 class ElectrumGui():
1405 def __init__(self, wallet):
1406 self.wallet = wallet
1407 self.app = QApplication(sys.argv)
1409 def waiting_dialog(self):
1415 w.setWindowTitle('Electrum')
1417 vbox = QVBoxLayout()
1422 if self.wallet.up_to_date:
1425 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1426 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1428 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1429 self.wallet.interface.poke()
1434 def restore_or_create(self):
1436 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1437 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1438 if r==2: return False
1440 is_recovery = (r==1)
1441 wallet = self.wallet
1442 # ask for the server.
1443 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1446 wallet.new_seed(None)
1447 wallet.init_mpk( wallet.seed )
1448 wallet.up_to_date_event.clear()
1449 wallet.up_to_date = False
1450 self.waiting_dialog()
1451 # run a dialog indicating the seed, ask the user to remember it
1452 ElectrumWindow.show_seed_dialog(wallet)
1454 ElectrumWindow.change_password_dialog(wallet)
1456 # ask for seed and gap.
1457 if not ElectrumWindow.seed_dialog( wallet ): return False
1458 wallet.init_mpk( wallet.seed )
1459 wallet.up_to_date_event.clear()
1460 wallet.up_to_date = False
1461 self.waiting_dialog()
1462 if wallet.is_found():
1463 # history and addressbook
1464 wallet.update_tx_history()
1465 wallet.fill_addressbook()
1466 print "recovery successful"
1469 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1477 w = ElectrumWindow(self.wallet)
1478 if url: w.set_url(url)