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 sys.stderr.write("Error: Could not import PyQt4\n")
26 sys.stderr.write("on Linux systems, you may try 'sudo apt-get install python-qt4'\n")
30 from PyQt4.QtGui import *
31 from PyQt4.QtCore import *
32 import PyQt4.QtCore as QtCore
33 import PyQt4.QtGui as QtGui
34 from interface import DEFAULT_SERVERS
39 sys.stderr.write("Error: Could not import icons_rc.py\n")
40 sys.stderr.write("Please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'\n")
44 from wallet import format_satoshis
45 import bmp, mnemonic, pyqrnative
47 from decimal import Decimal
51 if platform.system() == 'Windows':
52 MONOSPACE_FONT = 'Lucida Console'
53 elif platform.system() == 'Darwin':
54 MONOSPACE_FONT = 'Monaco'
56 MONOSPACE_FONT = 'monospace'
58 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
60 def numbify(entry, is_int = False):
61 text = unicode(entry.text()).strip()
63 if not is_int: chars +='.'
64 s = ''.join([i for i in text if i in chars])
69 s = s[:p] + '.' + s[p:p+8]
71 amount = int( Decimal(s) * 100000000 )
83 class Timer(QtCore.QThread):
86 self.emit(QtCore.SIGNAL('timersignal'))
89 class HelpButton(QPushButton):
90 def __init__(self, text):
91 QPushButton.__init__(self, '?')
92 self.setFocusPolicy(Qt.NoFocus)
93 self.setFixedWidth(20)
94 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
97 class EnterButton(QPushButton):
98 def __init__(self, text, func):
99 QPushButton.__init__(self, text)
101 self.clicked.connect(func)
103 def keyPressEvent(self, e):
104 if e.key() == QtCore.Qt.Key_Return:
107 class MyTreeWidget(QTreeWidget):
108 def __init__(self, parent):
109 QTreeWidget.__init__(self, parent)
112 for i in range(0,self.viewport().height()/5):
113 if self.itemAt(QPoint(0,i*5)) == item:
117 for j in range(0,30):
118 if self.itemAt(QPoint(0,i*5 + j)) != item:
120 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
122 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
127 class StatusBarButton(QPushButton):
128 def __init__(self, icon, tooltip, func):
129 QPushButton.__init__(self, icon, '')
130 self.setToolTip(tooltip)
132 self.setMaximumWidth(25)
133 self.clicked.connect(func)
136 def keyPressEvent(self, e):
137 if e.key() == QtCore.Qt.Key_Return:
141 class QRCodeWidget(QWidget):
143 def __init__(self, addr):
144 super(QRCodeWidget, self).__init__()
145 self.setGeometry(300, 300, 350, 350)
148 def set_addr(self, addr):
150 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
151 self.qr.addData(addr)
154 def paintEvent(self, e):
155 qp = QtGui.QPainter()
158 size = self.qr.getModuleCount()*boxsize
159 k = self.qr.getModuleCount()
160 black = QColor(0, 0, 0, 255)
161 white = QColor(255, 255, 255, 255)
164 if self.qr.isDark(r, c):
170 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
175 def ok_cancel_buttons(dialog):
178 b = QPushButton("OK")
180 b.clicked.connect(dialog.accept)
181 b = QPushButton("Cancel")
183 b.clicked.connect(dialog.reject)
187 class ElectrumWindow(QMainWindow):
189 def __init__(self, wallet):
190 QMainWindow.__init__(self)
192 self.wallet.register_callback(self.update_callback)
194 self.funds_error = False
195 self.completions = QStringListModel()
197 self.tabs = tabs = QTabWidget(self)
198 tabs.addTab(self.create_history_tab(), _('History') )
200 tabs.addTab(self.create_send_tab(), _('Send') )
201 tabs.addTab(self.create_receive_tab(), _('Receive') )
202 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
203 tabs.addTab(self.create_wall_tab(), _('Wall') )
204 tabs.setMinimumSize(600, 400)
205 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
206 self.setCentralWidget(tabs)
207 self.create_status_bar()
208 self.setGeometry(100,100,840,400)
209 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
210 if not self.wallet.seed: title += ' [seedless]'
211 self.setWindowTitle( title )
213 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
214 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
215 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
216 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
218 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
219 self.history_list.setFocus(True)
221 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
222 if platform.system() == 'Windows':
223 n = 3 if self.wallet.seed else 2
224 tabs.setCurrentIndex (n)
225 tabs.setCurrentIndex (0)
228 def connect_slots(self, sender):
230 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
231 self.previous_payto_e=''
233 def check_recipient(self):
234 if self.payto_e.hasFocus():
236 r = unicode( self.payto_e.text() )
237 if r != self.previous_payto_e:
238 self.previous_payto_e = r
240 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
242 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
246 s = r + ' <' + to_address + '>'
247 self.payto_e.setText(s)
250 def update_callback(self):
251 self.emit(QtCore.SIGNAL('updatesignal'))
253 def update_wallet(self):
254 if self.wallet.interface and self.wallet.interface.is_connected:
255 if self.wallet.blocks == -1:
256 text = _( "Connecting..." )
257 icon = QIcon(":icons/status_disconnected.png")
258 elif self.wallet.blocks == 0:
259 text = _( "Server not ready" )
260 icon = QIcon(":icons/status_disconnected.png")
261 elif not self.wallet.up_to_date:
262 text = _( "Synchronizing..." )
263 icon = QIcon(":icons/status_waiting.png")
265 c, u = self.wallet.get_balance()
266 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
267 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
268 icon = QIcon(":icons/status_connected.png")
270 text = _( "Not connected" )
271 icon = QIcon(":icons/status_disconnected.png")
274 text = _( "Not enough funds" )
276 self.statusBar().showMessage(text)
277 self.status_button.setIcon( icon )
279 if self.wallet.up_to_date:
280 self.textbox.setText( self.wallet.banner )
281 self.update_history_tab()
282 self.update_receive_tab()
283 self.update_contacts_tab()
284 self.update_completions()
287 def create_history_tab(self):
288 self.history_list = l = MyTreeWidget(self)
290 l.setColumnWidth(0, 40)
291 l.setColumnWidth(1, 140)
292 l.setColumnWidth(2, 350)
293 l.setColumnWidth(3, 140)
294 l.setColumnWidth(4, 140)
295 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
296 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
297 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
298 l.setContextMenuPolicy(Qt.CustomContextMenu)
299 l.customContextMenuRequested.connect(self.create_history_menu)
302 def create_history_menu(self, position):
303 self.history_list.selectedIndexes()
304 item = self.history_list.currentItem()
306 tx_hash = str(item.toolTip(0))
308 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
309 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
310 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
311 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
313 def tx_details(self, tx_hash):
314 tx = self.wallet.tx_history.get(tx_hash)
317 conf = self.wallet.blocks - tx['height'] + 1
318 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
323 tx_details = _("Transaction Details") +"\n\n" \
324 + "Transaction ID:\n" + tx_hash + "\n\n" \
325 + "Status: %d confirmations\n\n"%conf \
326 + "Date: %s\n\n"%time_str \
327 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
328 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
330 r = self.wallet.receipts.get(tx_hash)
332 tx_details += "\n_______________________________________" \
333 + '\n\nSigned URI: ' + r[2] \
334 + "\n\nSigned by: " + r[0] \
335 + '\n\nSignature: ' + r[1]
337 QMessageBox.information(self, 'Details', tx_details, 'OK')
340 def tx_label_clicked(self, item, column):
341 if column==2 and item.isSelected():
342 tx_hash = str(item.toolTip(0))
344 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
345 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
346 self.history_list.editItem( item, column )
347 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
350 def tx_label_changed(self, item, column):
354 tx_hash = str(item.toolTip(0))
355 tx = self.wallet.tx_history.get(tx_hash)
356 s = self.wallet.labels.get(tx_hash)
357 text = unicode( item.text(2) )
359 self.wallet.labels[tx_hash] = text
360 item.setForeground(2, QBrush(QColor('black')))
362 if s: self.wallet.labels.pop(tx_hash)
363 text = tx['default_label']
364 item.setText(2, text)
365 item.setForeground(2, QBrush(QColor('gray')))
368 def edit_label(self, is_recv):
369 l = self.receive_list if is_recv else self.contacts_list
370 c = 2 if is_recv else 1
371 item = l.currentItem()
372 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
373 l.editItem( item, c )
374 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
376 def address_label_clicked(self, item, column, l, column_addr, column_label):
377 if column==column_label and item.isSelected():
378 addr = unicode( item.text(column_addr) )
379 label = unicode( item.text(column_label) )
380 if label in self.wallet.aliases.keys():
382 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
383 l.editItem( item, column )
384 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
386 def address_label_changed(self, item, column, l, column_addr, column_label):
387 addr = unicode( item.text(column_addr) )
388 text = unicode( item.text(column_label) )
390 if text not in self.wallet.aliases.keys():
391 self.wallet.labels[addr] = text
393 sys.stderr.write("Error: This is one of your aliases\n")
395 label = self.wallet.labels.get(addr,'')
396 item.setText(column_label, QString(label))
398 s = self.wallet.labels.get(addr)
399 if s: self.wallet.labels.pop(addr)
401 self.update_history_tab()
402 self.update_completions()
404 def update_history_tab(self):
405 self.history_list.clear()
407 for tx in self.wallet.get_tx_history():
408 tx_hash = tx['tx_hash']
410 conf = self.wallet.blocks - tx['height'] + 1
411 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
412 icon = QIcon(":icons/confirmed.png")
416 icon = QIcon(":icons/unconfirmed.png")
419 label = self.wallet.labels.get(tx_hash)
420 is_default_label = (label == '') or (label is None)
421 if is_default_label: label = tx['default_label']
423 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
424 item.setFont(2, QFont(MONOSPACE_FONT))
425 item.setFont(3, QFont(MONOSPACE_FONT))
426 item.setFont(4, QFont(MONOSPACE_FONT))
427 item.setToolTip(0, tx_hash)
429 item.setForeground(2, QBrush(QColor('grey')))
431 item.setIcon(0, icon)
432 self.history_list.insertTopLevelItem(0,item)
434 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
437 def create_send_tab(self):
442 grid.setColumnMinimumWidth(3,300)
443 grid.setColumnStretch(5,1)
445 self.payto_e = QLineEdit()
446 grid.addWidget(QLabel(_('Pay to')), 1, 0)
447 grid.addWidget(self.payto_e, 1, 1, 1, 3)
448 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)
450 completer = QCompleter()
451 completer.setCaseSensitivity(False)
452 self.payto_e.setCompleter(completer)
453 completer.setModel(self.completions)
455 self.message_e = QLineEdit()
456 grid.addWidget(QLabel(_('Description')), 2, 0)
457 grid.addWidget(self.message_e, 2, 1, 1, 3)
458 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)
460 self.amount_e = QLineEdit()
461 grid.addWidget(QLabel(_('Amount')), 3, 0)
462 grid.addWidget(self.amount_e, 3, 1, 1, 2)
463 grid.addWidget(HelpButton(
464 _('Amount to be sent.') + '\n\n' \
465 + _('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)
467 self.fee_e = QLineEdit()
468 grid.addWidget(QLabel(_('Fee')), 4, 0)
469 grid.addWidget(self.fee_e, 4, 1, 1, 2)
470 grid.addWidget(HelpButton(
471 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
472 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
473 + _('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)
475 b = EnterButton(_("Send"), self.do_send)
476 grid.addWidget(b, 6, 1)
478 b = EnterButton(_("Clear"),self.do_clear)
479 grid.addWidget(b, 6, 2)
481 self.payto_sig = QLabel('')
482 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
484 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
485 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
494 def entry_changed( is_fee ):
495 self.funds_error = False
496 amount = numbify(self.amount_e)
497 fee = numbify(self.fee_e)
498 if not is_fee: fee = None
501 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
503 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
506 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
509 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
510 self.funds_error = True
511 self.amount_e.setPalette(palette)
512 self.fee_e.setPalette(palette)
514 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
515 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
520 def update_completions(self):
522 for addr,label in self.wallet.labels.items():
523 if addr in self.wallet.addressbook:
524 l.append( label + ' <' + addr + '>')
525 l = l + self.wallet.aliases.keys()
527 self.completions.setStringList(l)
533 label = unicode( self.message_e.text() )
534 r = unicode( self.payto_e.text() )
538 m1 = re.match(ALIAS_REGEXP, r)
539 # label or alias, with address in brackets
540 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
543 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
547 to_address = m2.group(2)
551 if not self.wallet.is_valid(to_address):
552 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
556 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
558 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
561 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
563 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
566 if self.wallet.use_encryption:
567 password = self.password_dialog()
574 tx = self.wallet.mktx( to_address, amount, label, password, fee)
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'), _('Tx')])
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 if address in self.wallet.addresses:
794 c, u = self.wallet.get_addr_balance(address)
795 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
796 flags = self.wallet.get_address_flags(address)
797 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
799 item.setFont(0, QFont(MONOSPACE_FONT))
800 item.setFont(1, QFont(MONOSPACE_FONT))
801 item.setFont(3, QFont(MONOSPACE_FONT))
802 if address in self.wallet.frozen_addresses:
803 item.setBackgroundColor(1, QColor('lightblue'))
804 elif address in self.wallet.prioritized_addresses:
805 item.setBackgroundColor(1, QColor('lightgreen'))
806 if is_red and address in self.wallet.addresses:
807 item.setBackgroundColor(1, QColor('red'))
808 l.addTopLevelItem(item)
810 # we use column 1 because column 0 may be hidden
811 l.setCurrentItem(l.topLevelItem(0),1)
813 def show_contact_details(self, m):
814 a = self.wallet.aliases.get(m)
816 if a[0] in self.wallet.authorities.keys():
817 s = self.wallet.authorities.get(a[0])
820 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
821 QMessageBox.information(self, 'Alias', msg, 'OK')
823 def update_contacts_tab(self):
825 l = self.contacts_list
827 l.setColumnHidden(2, not self.wallet.expert_mode)
828 l.setColumnWidth(0, 350)
829 l.setColumnWidth(1, 330)
830 l.setColumnWidth(2, 100)
833 for alias, v in self.wallet.aliases.items():
835 alias_targets.append(target)
836 item = QTreeWidgetItem( [ target, alias, '-'] )
837 item.setBackgroundColor(0, QColor('lightgray'))
838 l.addTopLevelItem(item)
840 for address in self.wallet.addressbook:
841 if address in alias_targets: continue
842 label = self.wallet.labels.get(address,'')
844 for item in self.wallet.tx_history.values():
845 if address in item['outputs'] : n=n+1
847 item = QTreeWidgetItem( [ address, label, tx] )
848 item.setFont(0, QFont(MONOSPACE_FONT))
849 l.addTopLevelItem(item)
851 l.setCurrentItem(l.topLevelItem(0))
853 def create_wall_tab(self):
854 self.textbox = textbox = QTextEdit(self)
855 textbox.setFont(QFont(MONOSPACE_FONT))
856 textbox.setReadOnly(True)
859 def create_status_bar(self):
861 sb.setFixedHeight(35)
863 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
864 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
866 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
867 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
868 sb.addPermanentWidget( self.status_button )
869 self.setStatusBar(sb)
871 def new_contact_dialog(self):
872 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
873 address = unicode(text)
875 if self.wallet.is_valid(address):
876 self.wallet.addressbook.append(address)
878 self.update_contacts_tab()
879 self.update_history_tab()
880 self.update_completions()
882 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
885 def show_seed_dialog(wallet, parent=None):
888 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
891 if wallet.use_encryption:
892 password = parent.password_dialog()
893 if not password: return
898 seed = wallet.pw_decode( wallet.seed, password)
900 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
903 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
904 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
905 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
906 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
910 d.setWindowTitle(_("Seed"))
911 d.setMinimumSize(400, 270)
915 vbox2 = QVBoxLayout()
917 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
920 hbox.addLayout(vbox2)
921 hbox.addWidget(QLabel(msg))
933 b = QPushButton(_("Copy to Clipboard"))
934 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
936 b = QPushButton(_("View as QR Code"))
937 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
940 b = QPushButton(_("OK"))
941 b.clicked.connect(d.accept)
948 def show_seed_qrcode(seed):
952 d.setWindowTitle(_("Seed"))
953 d.setMinimumSize(270, 300)
955 vbox.addWidget(QRCodeWidget(seed))
958 b = QPushButton(_("OK"))
960 b.clicked.connect(d.accept)
967 def show_address_qrcode(self,address):
968 if not address: return
971 d.setWindowTitle(address)
972 d.setMinimumSize(270, 350)
974 qrw = QRCodeWidget(address)
978 amount_e = QLineEdit()
979 hbox.addWidget(QLabel(_('Amount')))
980 hbox.addWidget(amount_e)
983 #hbox = QHBoxLayout()
984 #label_e = QLineEdit()
985 #hbox.addWidget(QLabel('Label'))
986 #hbox.addWidget(label_e)
987 #vbox.addLayout(hbox)
989 def amount_changed():
990 amount = numbify(amount_e)
991 #label = str( label_e.getText() )
992 if amount is not None:
993 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
995 qrw.set_addr( address )
999 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
1000 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
1002 amount_e.textChanged.connect( amount_changed )
1004 hbox = QHBoxLayout()
1006 b = QPushButton(_("Save"))
1007 b.clicked.connect(do_save)
1009 b = QPushButton(_("Close"))
1011 b.clicked.connect(d.accept)
1013 vbox.addLayout(hbox)
1017 def question(self, msg):
1018 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1020 def show_message(self, msg):
1021 QMessageBox.information(self, _('Message'), msg, _('OK'))
1023 def password_dialog(self ):
1030 vbox = QVBoxLayout()
1031 msg = _('Please enter your password')
1032 vbox.addWidget(QLabel(msg))
1034 grid = QGridLayout()
1036 grid.addWidget(QLabel(_('Password')), 1, 0)
1037 grid.addWidget(pw, 1, 1)
1038 vbox.addLayout(grid)
1040 vbox.addLayout(ok_cancel_buttons(d))
1043 if not d.exec_(): return
1044 return unicode(pw.text())
1051 def change_password_dialog( wallet, parent=None ):
1054 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1062 new_pw = QLineEdit()
1063 new_pw.setEchoMode(2)
1064 conf_pw = QLineEdit()
1065 conf_pw.setEchoMode(2)
1067 vbox = QVBoxLayout()
1069 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')
1071 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1072 vbox.addWidget(QLabel(msg))
1074 grid = QGridLayout()
1077 if wallet.use_encryption:
1078 grid.addWidget(QLabel(_('Password')), 1, 0)
1079 grid.addWidget(pw, 1, 1)
1081 grid.addWidget(QLabel(_('New Password')), 2, 0)
1082 grid.addWidget(new_pw, 2, 1)
1084 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1085 grid.addWidget(conf_pw, 3, 1)
1086 vbox.addLayout(grid)
1088 vbox.addLayout(ok_cancel_buttons(d))
1091 if not d.exec_(): return
1093 password = unicode(pw.text()) if wallet.use_encryption else None
1094 new_password = unicode(new_pw.text())
1095 new_password2 = unicode(conf_pw.text())
1098 seed = wallet.pw_decode( wallet.seed, password)
1100 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1103 if new_password != new_password2:
1104 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1107 wallet.update_password(seed, password, new_password)
1110 def seed_dialog(wallet, parent=None):
1114 vbox = QVBoxLayout()
1115 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1116 vbox.addWidget(QLabel(msg))
1118 grid = QGridLayout()
1121 seed_e = QLineEdit()
1122 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1123 grid.addWidget(seed_e, 1, 1)
1127 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1128 grid.addWidget(gap_e, 2, 1)
1129 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1130 vbox.addLayout(grid)
1132 vbox.addLayout(ok_cancel_buttons(d))
1135 if not d.exec_(): return
1138 gap = int(unicode(gap_e.text()))
1140 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1144 seed = unicode(seed_e.text())
1147 sys.stderr.write("Warning: Not hex, trying decode\n")
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 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1195 + _('Recommended value') + ': 0.001'
1196 grid.addWidget(HelpButton(msg), 2, 2)
1197 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1200 nz_e.setText("%d"% self.wallet.num_zeros)
1201 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1202 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1203 grid.addWidget(HelpButton(msg), 3, 2)
1204 grid.addWidget(nz_e, 3, 1)
1205 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1207 cb = QCheckBox(_('Expert mode'))
1208 grid.addWidget(cb, 4, 0)
1209 cb.setChecked(self.wallet.expert_mode)
1211 if self.wallet.expert_mode:
1213 usechange_cb = QCheckBox(_('Use change addresses'))
1214 grid.addWidget(usechange_cb, 5, 0)
1215 usechange_cb.setChecked(self.wallet.use_change)
1216 grid.addWidget(HelpButton(_('Using a change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1218 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1219 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1220 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1221 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1222 + _('Warning') + ': ' \
1223 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1224 + _('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'
1226 gap_e.setText("%d"% self.wallet.gap_limit)
1227 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1228 grid.addWidget(gap_e, 6, 1)
1229 grid.addWidget(HelpButton(msg), 6, 2)
1230 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1233 vbox.addLayout(ok_cancel_buttons(d))
1237 if not d.exec_(): return
1239 fee = unicode(fee_e.text())
1241 fee = int( 100000000 * Decimal(fee) )
1243 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1246 if self.wallet.fee != fee:
1247 self.wallet.fee = fee
1250 nz = unicode(nz_e.text())
1255 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1258 if self.wallet.num_zeros != nz:
1259 self.wallet.num_zeros = nz
1260 self.update_history_tab()
1261 self.update_receive_tab()
1264 if self.wallet.expert_mode:
1266 self.wallet.use_change = usechange_cb.isChecked()
1269 n = int(gap_e.text())
1271 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1273 if self.wallet.gap_limit != n:
1274 r = self.wallet.change_gap_limit(n)
1276 self.update_receive_tab()
1278 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1280 self.set_expert_mode(cb.isChecked())
1284 def network_dialog(wallet, parent=None):
1285 interface = wallet.interface
1287 if interface.is_connected:
1288 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1290 status = _("Not connected")
1291 server = wallet.server
1294 status = _("Please choose a server.")
1295 server = random.choice( DEFAULT_SERVERS )
1297 if not wallet.interface.servers:
1299 for x in DEFAULT_SERVERS:
1300 h,port,protocol = x.split(':')
1301 servers_list.append( (h,[(protocol,port)] ) )
1303 servers_list = wallet.interface.servers
1306 for item in servers_list:
1310 protocol, port = item2
1316 d.setWindowTitle(_('Server'))
1317 d.setMinimumSize(375, 20)
1319 vbox = QVBoxLayout()
1322 hbox = QHBoxLayout()
1324 l.setPixmap(QPixmap(":icons/network.png"))
1326 hbox.addWidget(QLabel(status))
1328 vbox.addLayout(hbox)
1330 hbox = QHBoxLayout()
1331 host_line = QLineEdit()
1332 host_line.setText(server)
1333 hbox.addWidget(QLabel(_('Connect to') + ':'))
1334 hbox.addWidget(host_line)
1335 vbox.addLayout(hbox)
1337 hbox = QHBoxLayout()
1339 buttonGroup = QGroupBox(_("Protocol"))
1340 radio1 = QRadioButton("tcp", buttonGroup)
1341 radio2 = QRadioButton("http", buttonGroup)
1344 return unicode(host_line.text()).split(':')
1346 def set_button(protocol):
1348 radio1.setChecked(1)
1349 elif protocol == 'h':
1350 radio2.setChecked(1)
1352 def set_protocol(protocol):
1353 host = current_line()[0]
1355 if protocol not in pp.keys():
1356 protocol = pp.keys()[0]
1357 set_button(protocol)
1359 host_line.setText( host + ':' + port + ':' + protocol)
1361 radio1.clicked.connect(lambda x: set_protocol('t') )
1362 radio2.clicked.connect(lambda x: set_protocol('h') )
1364 set_button(current_line()[2])
1366 hbox.addWidget(QLabel(_('Protocol')+':'))
1367 hbox.addWidget(radio1)
1368 hbox.addWidget(radio2)
1370 vbox.addLayout(hbox)
1372 if wallet.interface.servers:
1373 label = _('Active Servers')
1375 label = _('Default Servers')
1377 servers_list_widget = QTreeWidget(parent)
1378 servers_list_widget.setHeaderLabels( [ label ] )
1379 servers_list_widget.setMaximumHeight(150)
1380 for host in plist.keys():
1381 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1384 host = unicode(x.text(0))
1386 if 't' in pp.keys():
1389 protocol = pp.keys()[0]
1391 host_line.setText( host + ':' + port + ':' + protocol)
1392 set_button(protocol)
1394 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1395 vbox.addWidget(servers_list_widget)
1397 vbox.addLayout(ok_cancel_buttons(d))
1400 if not d.exec_(): return
1401 server = unicode( host_line.text() )
1404 wallet.set_server(server)
1406 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1418 def __init__(self, wallet, app=None):
1419 self.wallet = wallet
1421 self.app = QApplication(sys.argv)
1423 def waiting_dialog(self):
1429 w.setWindowTitle('Electrum')
1431 vbox = QVBoxLayout()
1436 if self.wallet.up_to_date:
1439 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1440 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1442 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1443 self.wallet.interface.poke()
1448 def restore_or_create(self):
1450 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1451 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1452 if r==2: return False
1454 is_recovery = (r==1)
1455 wallet = self.wallet
1456 # ask for the server.
1457 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1460 wallet.new_seed(None)
1461 wallet.init_mpk( wallet.seed )
1462 wallet.up_to_date_event.clear()
1463 wallet.up_to_date = False
1464 self.waiting_dialog()
1465 # run a dialog indicating the seed, ask the user to remember it
1466 ElectrumWindow.show_seed_dialog(wallet)
1468 ElectrumWindow.change_password_dialog(wallet)
1470 # ask for seed and gap.
1471 if not ElectrumWindow.seed_dialog( wallet ): return False
1472 wallet.init_mpk( wallet.seed )
1473 wallet.up_to_date_event.clear()
1474 wallet.up_to_date = False
1475 self.waiting_dialog()
1476 if wallet.is_found():
1477 # history and addressbook
1478 wallet.update_tx_history()
1479 wallet.fill_addressbook()
1480 print "Recovery successful"
1483 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1491 w = ElectrumWindow(self.wallet)
1492 if url: w.set_url(url)