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\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)
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\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)
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(_('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)
456 self.fee_e = QLineEdit()
457 grid.addWidget(QLabel(_('Fee')), 4, 0)
458 grid.addWidget(self.fee_e, 4, 1, 1, 2)
459 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)
461 b = EnterButton(_("Send"), self.do_send)
462 grid.addWidget(b, 6, 1)
464 b = EnterButton(_("Clear"),self.do_clear)
465 grid.addWidget(b, 6, 2)
467 self.payto_sig = QLabel('')
468 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
470 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
471 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
481 def entry_changed( is_fee ):
482 self.funds_error = False
483 amount = numbify(self.amount_e)
484 fee = numbify(self.fee_e)
485 if not is_fee: fee = None
488 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
490 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
493 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
496 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
497 self.funds_error = True
498 self.amount_e.setPalette(palette)
499 self.fee_e.setPalette(palette)
501 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
502 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
507 def update_completions(self):
509 for addr,label in self.wallet.labels.items():
510 if addr in self.wallet.addressbook:
511 l.append( label + ' <' + addr + '>')
512 l = l + self.wallet.aliases.keys()
514 self.completions.setStringList(l)
520 label = unicode( self.message_e.text() )
521 r = unicode( self.payto_e.text() )
525 m1 = re.match(ALIAS_REGEXP, r)
526 # label or alias, with address in brackets
527 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
530 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
534 to_address = m2.group(2)
538 if not self.wallet.is_valid(to_address):
539 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
543 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
545 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
548 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
550 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
553 if self.wallet.use_encryption:
554 password = self.password_dialog()
561 tx = self.wallet.mktx( to_address, amount, label, password, fee)
562 except BaseException, e:
563 self.show_message(str(e))
566 status, msg = self.wallet.sendtx( tx )
568 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
570 self.update_contacts_tab()
572 QMessageBox.warning(self, _('Error'), msg, _('OK'))
575 def set_url(self, url):
576 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
577 self.tabs.setCurrentIndex(1)
578 label = self.wallet.labels.get(payto)
579 m_addr = label + ' <'+ payto+'>' if label else payto
580 self.payto_e.setText(m_addr)
582 self.message_e.setText(message)
583 self.amount_e.setText(amount)
585 self.set_frozen(self.payto_e,True)
586 self.set_frozen(self.amount_e,True)
587 self.set_frozen(self.message_e,True)
588 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
590 self.payto_sig.setVisible(False)
593 self.payto_sig.setVisible(False)
594 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
596 self.set_frozen(e,False)
598 def set_frozen(self,entry,frozen):
600 entry.setReadOnly(True)
601 entry.setFrame(False)
603 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
604 entry.setPalette(palette)
606 entry.setReadOnly(False)
609 palette.setColor(entry.backgroundRole(), QColor('white'))
610 entry.setPalette(palette)
613 def toggle_freeze(self,addr):
615 if addr in self.wallet.frozen_addresses:
616 self.wallet.unfreeze(addr)
618 self.wallet.freeze(addr)
619 self.update_receive_tab()
621 def toggle_priority(self,addr):
623 if addr in self.wallet.prioritized_addresses:
624 self.wallet.unprioritize(addr)
626 self.wallet.prioritize(addr)
627 self.update_receive_tab()
630 def create_list_tab(self, headers):
631 "generic tab creation method"
632 l = MyTreeWidget(self)
633 l.setColumnCount( len(headers) )
634 l.setHeaderLabels( headers )
644 vbox.addWidget(buttons)
649 buttons.setLayout(hbox)
654 def create_receive_tab(self):
655 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
656 l.setContextMenuPolicy(Qt.CustomContextMenu)
657 l.customContextMenuRequested.connect(self.create_receive_menu)
658 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
659 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
660 self.receive_list = l
661 self.receive_buttons_hbox = hbox
665 def create_contacts_tab(self):
666 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
667 l.setContextMenuPolicy(Qt.CustomContextMenu)
668 l.customContextMenuRequested.connect(self.create_contact_menu)
669 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
670 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
671 self.contacts_list = l
672 self.contacts_buttons_hbox = hbox
673 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
678 def create_receive_menu(self, position):
679 # fixme: this function apparently has a side effect.
680 # if it is not called the menu pops up several times
681 #self.receive_list.selectedIndexes()
683 item = self.receive_list.itemAt(position)
685 addr = unicode(item.text(1))
687 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
688 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
689 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
690 if self.wallet.expert_mode:
691 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
692 menu.addAction(t, lambda: self.toggle_freeze(addr))
693 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
694 menu.addAction(t, lambda: self.toggle_priority(addr))
695 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
698 def payto(self, x, is_alias):
705 label = self.wallet.labels.get(addr)
706 m_addr = label + ' <' + addr + '>' if label else addr
707 self.tabs.setCurrentIndex(1)
708 self.payto_e.setText(m_addr)
709 self.amount_e.setFocus()
711 def delete_contact(self, x, is_alias):
712 if self.question("Do you want to remove %s from your list of contacts?"%x):
713 if not is_alias and x in self.wallet.addressbook:
714 self.wallet.addressbook.remove(x)
715 if x in self.wallet.labels.keys():
716 self.wallet.labels.pop(x)
717 elif is_alias and x in self.wallet.aliases:
718 self.wallet.aliases.pop(x)
719 self.update_history_tab()
720 self.update_contacts_tab()
721 self.update_completions()
723 def create_contact_menu(self, position):
724 # fixme: this function apparently has a side effect.
725 # if it is not called the menu pops up several times
726 #self.contacts_list.selectedIndexes()
728 item = self.contacts_list.itemAt(position)
730 addr = unicode(item.text(0))
731 label = unicode(item.text(1))
732 is_alias = label in self.wallet.aliases.keys()
733 x = label if is_alias else addr
735 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
736 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
737 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
739 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
741 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
742 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
743 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
746 def update_receive_tab(self):
747 l = self.receive_list
749 l.setColumnHidden(0,not self.wallet.expert_mode)
750 l.setColumnHidden(3,not self.wallet.expert_mode)
751 l.setColumnHidden(4,not self.wallet.expert_mode)
752 l.setColumnWidth(0, 50)
753 l.setColumnWidth(1, 310)
754 l.setColumnWidth(2, 250)
755 l.setColumnWidth(3, 130)
756 l.setColumnWidth(4, 10)
760 for address in self.wallet.all_addresses():
762 if self.wallet.is_change(address) and not self.wallet.expert_mode:
765 label = self.wallet.labels.get(address,'')
767 h = self.wallet.history.get(address,[])
769 if not item['is_input'] : n=n+1
773 if address in self.wallet.addresses:
775 if gap > self.wallet.gap_limit:
778 if address in self.wallet.addresses:
781 c, u = self.wallet.get_addr_balance(address)
782 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
783 flags = self.wallet.get_address_flags(address)
784 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
786 item.setFont(0, QFont(MONOSPACE_FONT))
787 item.setFont(1, QFont(MONOSPACE_FONT))
788 item.setFont(3, QFont(MONOSPACE_FONT))
789 if address in self.wallet.frozen_addresses:
790 item.setBackgroundColor(1, QColor('lightblue'))
791 elif address in self.wallet.prioritized_addresses:
792 item.setBackgroundColor(1, QColor('lightgreen'))
793 if is_red and address in self.wallet.addresses:
794 item.setBackgroundColor(1, QColor('red'))
795 l.addTopLevelItem(item)
797 # we use column 1 because column 0 may be hidden
798 l.setCurrentItem(l.topLevelItem(0),1)
800 def show_contact_details(self, m):
801 a = self.wallet.aliases.get(m)
803 if a[0] in self.wallet.authorities.keys():
804 s = self.wallet.authorities.get(a[0])
807 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
808 QMessageBox.information(self, 'Alias', msg, 'OK')
810 def update_contacts_tab(self):
812 l = self.contacts_list
814 l.setColumnHidden(2, not self.wallet.expert_mode)
815 l.setColumnWidth(0, 350)
816 l.setColumnWidth(1, 330)
817 l.setColumnWidth(2, 100)
820 for alias, v in self.wallet.aliases.items():
822 alias_targets.append(target)
823 item = QTreeWidgetItem( [ target, alias, '-'] )
824 item.setBackgroundColor(0, QColor('lightgray'))
825 l.addTopLevelItem(item)
827 for address in self.wallet.addressbook:
828 if address in alias_targets: continue
829 label = self.wallet.labels.get(address,'')
831 for item in self.wallet.tx_history.values():
832 if address in item['outputs'] : n=n+1
834 item = QTreeWidgetItem( [ address, label, tx] )
835 item.setFont(0, QFont(MONOSPACE_FONT))
836 l.addTopLevelItem(item)
838 l.setCurrentItem(l.topLevelItem(0))
840 def create_wall_tab(self):
841 self.textbox = textbox = QTextEdit(self)
842 textbox.setFont(QFont(MONOSPACE_FONT))
843 textbox.setReadOnly(True)
846 def create_status_bar(self):
848 sb.setFixedHeight(35)
850 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
851 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
853 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
854 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
855 sb.addPermanentWidget( self.status_button )
856 self.setStatusBar(sb)
858 def new_contact_dialog(self):
859 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
860 address = unicode(text)
862 if self.wallet.is_valid(address):
863 self.wallet.addressbook.append(address)
865 self.update_contacts_tab()
866 self.update_history_tab()
867 self.update_completions()
869 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
872 def show_seed_dialog(wallet, parent=None):
875 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
878 if wallet.use_encryption:
879 password = parent.password_dialog()
880 if not password: return
885 seed = wallet.pw_decode( wallet.seed, password)
887 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
890 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
891 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
892 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
893 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
897 d.setWindowTitle(_("Seed"))
898 d.setMinimumSize(400, 270)
902 vbox2 = QVBoxLayout()
904 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
907 hbox.addLayout(vbox2)
908 hbox.addWidget(QLabel(msg))
920 b = QPushButton(_("Copy to Clipboard"))
921 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
923 b = QPushButton(_("View as QR Code"))
924 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
927 b = QPushButton(_("OK"))
928 b.clicked.connect(d.accept)
935 def show_seed_qrcode(seed):
939 d.setWindowTitle(_("Seed"))
940 d.setMinimumSize(270, 300)
942 vbox.addWidget(QRCodeWidget(seed))
945 b = QPushButton(_("OK"))
947 b.clicked.connect(d.accept)
954 def show_address_qrcode(self,address):
955 if not address: return
958 d.setWindowTitle(address)
959 d.setMinimumSize(270, 350)
961 qrw = QRCodeWidget(address)
965 amount_e = QLineEdit()
966 hbox.addWidget(QLabel(_('Amount')))
967 hbox.addWidget(amount_e)
970 #hbox = QHBoxLayout()
971 #label_e = QLineEdit()
972 #hbox.addWidget(QLabel('Label'))
973 #hbox.addWidget(label_e)
974 #vbox.addLayout(hbox)
976 def amount_changed():
977 amount = numbify(amount_e)
978 #label = str( label_e.getText() )
979 if amount is not None:
980 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
982 qrw.set_addr( address )
986 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
987 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
989 amount_e.textChanged.connect( amount_changed )
993 b = QPushButton(_("Save"))
994 b.clicked.connect(do_save)
996 b = QPushButton(_("Close"))
998 b.clicked.connect(d.accept)
1000 vbox.addLayout(hbox)
1004 def question(self, msg):
1005 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1007 def show_message(self, msg):
1008 QMessageBox.information(self, _('Message'), msg, _('OK'))
1010 def password_dialog(self ):
1017 vbox = QVBoxLayout()
1018 msg = _('Please enter your password')
1019 vbox.addWidget(QLabel(msg))
1021 grid = QGridLayout()
1023 grid.addWidget(QLabel(_('Password')), 1, 0)
1024 grid.addWidget(pw, 1, 1)
1025 vbox.addLayout(grid)
1027 vbox.addLayout(ok_cancel_buttons(d))
1030 if not d.exec_(): return
1031 return unicode(pw.text())
1038 def change_password_dialog( wallet, parent=None ):
1041 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1049 new_pw = QLineEdit()
1050 new_pw.setEchoMode(2)
1051 conf_pw = QLineEdit()
1052 conf_pw.setEchoMode(2)
1054 vbox = QVBoxLayout()
1056 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')
1058 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1059 vbox.addWidget(QLabel(msg))
1061 grid = QGridLayout()
1064 if wallet.use_encryption:
1065 grid.addWidget(QLabel(_('Password')), 1, 0)
1066 grid.addWidget(pw, 1, 1)
1068 grid.addWidget(QLabel(_('New Password')), 2, 0)
1069 grid.addWidget(new_pw, 2, 1)
1071 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1072 grid.addWidget(conf_pw, 3, 1)
1073 vbox.addLayout(grid)
1075 vbox.addLayout(ok_cancel_buttons(d))
1078 if not d.exec_(): return
1080 password = unicode(pw.text()) if wallet.use_encryption else None
1081 new_password = unicode(new_pw.text())
1082 new_password2 = unicode(conf_pw.text())
1085 seed = wallet.pw_decode( wallet.seed, password)
1087 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1090 if new_password != new_password2:
1091 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1094 wallet.update_password(seed, password, new_password)
1097 def seed_dialog(wallet, parent=None):
1101 vbox = QVBoxLayout()
1102 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1103 vbox.addWidget(QLabel(msg))
1105 grid = QGridLayout()
1108 seed_e = QLineEdit()
1109 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1110 grid.addWidget(seed_e, 1, 1)
1114 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1115 grid.addWidget(gap_e, 2, 1)
1116 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1117 vbox.addLayout(grid)
1119 vbox.addLayout(ok_cancel_buttons(d))
1122 if not d.exec_(): return
1125 gap = int(unicode(gap_e.text()))
1127 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1131 seed = unicode(seed_e.text())
1134 print "not hex, trying decode"
1136 seed = mnemonic.mn_decode( seed.split(' ') )
1138 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1141 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1144 wallet.seed = str(seed)
1145 #print repr(wallet.seed)
1146 wallet.gap_limit = gap
1150 def set_expert_mode(self, b):
1151 self.wallet.expert_mode = b
1153 self.update_receive_tab()
1154 self.update_contacts_tab()
1155 # if self.wallet.seed:
1156 # self.nochange_cb.setHidden(not self.wallet.expert_mode)
1159 def settings_dialog(self):
1162 vbox = QVBoxLayout()
1163 msg = _('Here are the settings of your wallet.') + '\n'\
1164 + _('For more explanations, click on the help buttons next to each field.')
1167 label.setFixedWidth(250)
1168 label.setWordWrap(True)
1169 label.setAlignment(Qt.AlignJustify)
1170 vbox.addWidget(label)
1172 grid = QGridLayout()
1174 vbox.addLayout(grid)
1177 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1178 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1179 grid.addWidget(fee_e, 2, 1)
1180 grid.addWidget(HelpButton(_('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee. Recommended value: 0.001')), 2, 2)
1181 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1184 nz_e.setText("%d"% self.wallet.num_zeros)
1185 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1186 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)
1187 grid.addWidget(nz_e, 3, 1)
1188 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1190 cb = QCheckBox(_('Expert mode'))
1191 grid.addWidget(cb, 4, 0)
1192 cb.setChecked(self.wallet.expert_mode)
1194 if self.wallet.expert_mode:
1196 nochange_cb = QCheckBox(_('Use change addresses'))
1197 grid.addWidget(nochange_cb, 5, 0)
1198 nochange_cb.setChecked(self.wallet.use_change)
1200 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1201 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1202 + _('Your current gap limit is: ') + '%d'%self.wallet.gap_limit + '\n' \
1203 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1204 + _('Warning:') + ' ' \
1205 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1206 + _('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'
1208 gap_e.setText("%d"% self.wallet.gap_limit)
1209 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1210 grid.addWidget(gap_e, 6, 1)
1211 grid.addWidget(HelpButton(msg), 6, 2)
1212 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1215 vbox.addLayout(ok_cancel_buttons(d))
1219 if not d.exec_(): return
1221 fee = unicode(fee_e.text())
1223 fee = int( 100000000 * Decimal(fee) )
1225 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1228 if self.wallet.fee != fee:
1229 self.wallet.fee = fee
1232 nz = unicode(nz_e.text())
1237 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1240 if self.wallet.num_zeros != nz:
1241 self.wallet.num_zeros = nz
1242 self.update_history_tab()
1243 self.update_receive_tab()
1246 if self.wallet.expert_mode:
1248 self.wallet.nochange = nochange_cb.isChecked()
1251 n = int(gap_e.text())
1253 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1255 if self.wallet.gap_limit != n:
1256 r = self.wallet.change_gap_limit(n)
1258 self.update_receive_tab()
1260 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1262 self.set_expert_mode(cb.isChecked())
1266 def network_dialog(wallet, parent=None):
1267 interface = wallet.interface
1269 if interface.is_connected:
1270 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1272 status = _("Not connected")
1273 server = wallet.server
1276 status = _("Please choose a server.")
1277 server = random.choice( DEFAULT_SERVERS )
1279 if not wallet.interface.servers:
1281 for x in DEFAULT_SERVERS:
1282 h,port,protocol = x.split(':')
1283 servers_list.append( (h,[(protocol,port)] ) )
1285 servers_list = wallet.interface.servers
1288 for item in servers_list:
1292 protocol, port = item2
1298 d.setWindowTitle(_('Server'))
1299 d.setMinimumSize(375, 20)
1301 vbox = QVBoxLayout()
1304 hbox = QHBoxLayout()
1306 l.setPixmap(QPixmap(":icons/network.png"))
1308 hbox.addWidget(QLabel(status))
1310 vbox.addLayout(hbox)
1312 hbox = QHBoxLayout()
1313 host_line = QLineEdit()
1314 host_line.setText(server)
1315 hbox.addWidget(QLabel(_('Connect to') + ':'))
1316 hbox.addWidget(host_line)
1317 vbox.addLayout(hbox)
1319 hbox = QHBoxLayout()
1321 buttonGroup = QGroupBox(_("Protocol"))
1322 radio1 = QRadioButton("tcp", buttonGroup)
1323 radio2 = QRadioButton("http", buttonGroup)
1326 return unicode(host_line.text()).split(':')
1328 def set_button(protocol):
1330 radio1.setChecked(1)
1331 elif protocol == 'h':
1332 radio2.setChecked(1)
1334 def set_protocol(protocol):
1335 host = current_line()[0]
1337 if protocol not in pp.keys():
1338 protocol = pp.keys()[0]
1339 set_button(protocol)
1341 host_line.setText( host + ':' + port + ':' + protocol)
1343 radio1.clicked.connect(lambda x: set_protocol('t') )
1344 radio2.clicked.connect(lambda x: set_protocol('h') )
1346 set_button(current_line()[2])
1348 hbox.addWidget(QLabel(_('Protocol')+':'))
1349 hbox.addWidget(radio1)
1350 hbox.addWidget(radio2)
1352 vbox.addLayout(hbox)
1354 if wallet.interface.servers:
1355 label = _('Active Servers')
1357 label = _('Default Servers')
1359 servers_list_widget = QTreeWidget(parent)
1360 servers_list_widget.setHeaderLabels( [ label ] )
1361 servers_list_widget.setMaximumHeight(150)
1362 for host in plist.keys():
1363 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1366 host = unicode(x.text(0))
1368 if 't' in pp.keys():
1371 protocol = pp.keys()[0]
1373 host_line.setText( host + ':' + port + ':' + protocol)
1374 set_button(protocol)
1376 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1377 vbox.addWidget(servers_list_widget)
1379 vbox.addLayout(ok_cancel_buttons(d))
1382 if not d.exec_(): return
1383 server = unicode( host_line.text() )
1386 wallet.set_server(server)
1388 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1398 class ElectrumGui():
1400 def __init__(self, wallet):
1401 self.wallet = wallet
1402 self.app = QApplication(sys.argv)
1404 def waiting_dialog(self):
1410 w.setWindowTitle('Electrum')
1412 vbox = QVBoxLayout()
1417 if self.wallet.up_to_date:
1420 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1421 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1423 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1424 self.wallet.interface.poke()
1429 def restore_or_create(self):
1431 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1432 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1433 if r==2: return False
1435 is_recovery = (r==1)
1436 wallet = self.wallet
1437 # ask for the server.
1438 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1441 wallet.new_seed(None)
1442 wallet.init_mpk( wallet.seed )
1443 wallet.up_to_date_event.clear()
1444 wallet.up_to_date = False
1445 self.waiting_dialog()
1446 # run a dialog indicating the seed, ask the user to remember it
1447 ElectrumWindow.show_seed_dialog(wallet)
1449 ElectrumWindow.change_password_dialog(wallet)
1451 # ask for seed and gap.
1452 if not ElectrumWindow.seed_dialog( wallet ): return False
1453 wallet.init_mpk( wallet.seed )
1454 wallet.up_to_date_event.clear()
1455 wallet.up_to_date = False
1456 self.waiting_dialog()
1457 if wallet.is_found():
1458 # history and addressbook
1459 wallet.update_tx_history()
1460 wallet.fill_addressbook()
1461 print "recovery successful"
1464 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1472 w = ElectrumWindow(self.wallet)
1473 if url: w.set_url(url)