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
21 from util import print_error
26 sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'")
28 from PyQt4.QtGui import *
29 from PyQt4.QtCore import *
30 import PyQt4.QtCore as QtCore
31 import PyQt4.QtGui as QtGui
32 from interface import DEFAULT_SERVERS
37 sys.exit("Error: Could not import icons_rc.py, please generate it with: 'pyrcc4 icons.qrc -o lib/icons_rc.py'")
39 from wallet import format_satoshis
40 import bmp, mnemonic, pyqrnative, qrscanner
42 from decimal import Decimal
46 if platform.system() == 'Windows':
47 MONOSPACE_FONT = 'Lucida Console'
48 elif platform.system() == 'Darwin':
49 MONOSPACE_FONT = 'Monaco'
51 MONOSPACE_FONT = 'monospace'
53 ALIAS_REGEXP = '^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$'
55 def numbify(entry, is_int = False):
56 text = unicode(entry.text()).strip()
58 if not is_int: chars +='.'
59 s = ''.join([i for i in text if i in chars])
64 s = s[:p] + '.' + s[p:p+8]
66 amount = int( Decimal(s) * 100000000 )
78 class Timer(QtCore.QThread):
81 self.emit(QtCore.SIGNAL('timersignal'))
84 class HelpButton(QPushButton):
85 def __init__(self, text):
86 QPushButton.__init__(self, '?')
87 self.setFocusPolicy(Qt.NoFocus)
88 self.setFixedWidth(20)
89 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
92 class EnterButton(QPushButton):
93 def __init__(self, text, func):
94 QPushButton.__init__(self, text)
96 self.clicked.connect(func)
98 def keyPressEvent(self, e):
99 if e.key() == QtCore.Qt.Key_Return:
102 class MyTreeWidget(QTreeWidget):
103 def __init__(self, parent):
104 QTreeWidget.__init__(self, parent)
107 for i in range(0,self.viewport().height()/5):
108 if self.itemAt(QPoint(0,i*5)) == item:
112 for j in range(0,30):
113 if self.itemAt(QPoint(0,i*5 + j)) != item:
115 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
117 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
122 class StatusBarButton(QPushButton):
123 def __init__(self, icon, tooltip, func):
124 QPushButton.__init__(self, icon, '')
125 self.setToolTip(tooltip)
127 self.setMaximumWidth(25)
128 self.clicked.connect(func)
131 def keyPressEvent(self, e):
132 if e.key() == QtCore.Qt.Key_Return:
136 class QRCodeWidget(QWidget):
138 def __init__(self, addr):
139 super(QRCodeWidget, self).__init__()
140 self.setGeometry(300, 300, 350, 350)
143 def set_addr(self, addr):
145 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
146 self.qr.addData(addr)
149 def paintEvent(self, e):
150 qp = QtGui.QPainter()
153 size = self.qr.getModuleCount()*boxsize
154 k = self.qr.getModuleCount()
155 black = QColor(0, 0, 0, 255)
156 white = QColor(255, 255, 255, 255)
159 if self.qr.isDark(r, c):
165 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
170 def ok_cancel_buttons(dialog):
173 b = QPushButton("OK")
175 b.clicked.connect(dialog.accept)
176 b = QPushButton("Cancel")
178 b.clicked.connect(dialog.reject)
182 class ElectrumWindow(QMainWindow):
184 def __init__(self, wallet):
185 QMainWindow.__init__(self)
187 self.wallet.register_callback(self.update_callback)
189 self.funds_error = False
190 self.completions = QStringListModel()
192 self.tabs = tabs = QTabWidget(self)
193 tabs.addTab(self.create_history_tab(), _('History') )
195 tabs.addTab(self.create_send_tab(), _('Send') )
196 tabs.addTab(self.create_receive_tab(), _('Receive') )
197 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
198 tabs.addTab(self.create_wall_tab(), _('Wall') )
199 tabs.setMinimumSize(600, 400)
200 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
201 self.setCentralWidget(tabs)
202 self.create_status_bar()
203 self.setGeometry(100,100,840,400)
204 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
205 if not self.wallet.seed: title += ' [seedless]'
206 self.setWindowTitle( title )
208 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
209 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
210 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
211 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
213 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
214 self.history_list.setFocus(True)
216 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
217 if platform.system() == 'Windows':
218 n = 3 if self.wallet.seed else 2
219 tabs.setCurrentIndex (n)
220 tabs.setCurrentIndex (0)
223 def connect_slots(self, sender):
225 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
226 self.previous_payto_e=''
228 def check_recipient(self):
229 if self.payto_e.hasFocus():
231 r = unicode( self.payto_e.text() )
232 if r != self.previous_payto_e:
233 self.previous_payto_e = r
235 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
237 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
241 s = r + ' <' + to_address + '>'
242 self.payto_e.setText(s)
245 def update_callback(self):
246 self.emit(QtCore.SIGNAL('updatesignal'))
248 def update_wallet(self):
249 if self.wallet.interface and self.wallet.interface.is_connected:
250 if self.wallet.blocks == -1:
251 text = _( "Connecting..." )
252 icon = QIcon(":icons/status_disconnected.png")
253 elif self.wallet.blocks == 0:
254 text = _( "Server not ready" )
255 icon = QIcon(":icons/status_disconnected.png")
256 elif not self.wallet.up_to_date:
257 text = _( "Synchronizing..." )
258 icon = QIcon(":icons/status_waiting.png")
260 c, u = self.wallet.get_balance()
261 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
262 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
263 icon = QIcon(":icons/status_connected.png")
265 text = _( "Not connected" )
266 icon = QIcon(":icons/status_disconnected.png")
269 text = _( "Not enough funds" )
271 self.statusBar().showMessage(text)
272 self.status_button.setIcon( icon )
274 if self.wallet.up_to_date:
275 self.textbox.setText( self.wallet.banner )
276 self.update_history_tab()
277 self.update_receive_tab()
278 self.update_contacts_tab()
279 self.update_completions()
282 def create_history_tab(self):
283 self.history_list = l = MyTreeWidget(self)
285 l.setColumnWidth(0, 40)
286 l.setColumnWidth(1, 140)
287 l.setColumnWidth(2, 350)
288 l.setColumnWidth(3, 140)
289 l.setColumnWidth(4, 140)
290 l.setHeaderLabels( [ '', _( 'Date' ), _( 'To / From' ) , _('Amount'), _('Balance')] )
291 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
292 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
293 l.setContextMenuPolicy(Qt.CustomContextMenu)
294 l.customContextMenuRequested.connect(self.create_history_menu)
297 def create_history_menu(self, position):
298 self.history_list.selectedIndexes()
299 item = self.history_list.currentItem()
301 tx_hash = str(item.toolTip(0))
303 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
304 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
305 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
306 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
308 def tx_details(self, tx_hash):
309 tx = self.wallet.tx_history.get(tx_hash)
312 conf = self.wallet.blocks - tx['height'] + 1
313 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
318 tx_details = _("Transaction Details") +"\n\n" \
319 + "Transaction ID:\n" + tx_hash + "\n\n" \
320 + "Status: %d confirmations\n\n"%conf \
321 + "Date: %s\n\n"%time_str \
322 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
323 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
325 r = self.wallet.receipts.get(tx_hash)
327 tx_details += "\n_______________________________________" \
328 + '\n\nSigned URI: ' + r[2] \
329 + "\n\nSigned by: " + r[0] \
330 + '\n\nSignature: ' + r[1]
332 QMessageBox.information(self, 'Details', tx_details, 'OK')
335 def tx_label_clicked(self, item, column):
336 if column==2 and item.isSelected():
337 tx_hash = str(item.toolTip(0))
339 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
340 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
341 self.history_list.editItem( item, column )
342 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
345 def tx_label_changed(self, item, column):
349 tx_hash = str(item.toolTip(0))
350 tx = self.wallet.tx_history.get(tx_hash)
351 s = self.wallet.labels.get(tx_hash)
352 text = unicode( item.text(2) )
354 self.wallet.labels[tx_hash] = text
355 item.setForeground(2, QBrush(QColor('black')))
357 if s: self.wallet.labels.pop(tx_hash)
358 text = tx['default_label']
359 item.setText(2, text)
360 item.setForeground(2, QBrush(QColor('gray')))
363 def edit_label(self, is_recv):
364 l = self.receive_list if is_recv else self.contacts_list
365 c = 2 if is_recv else 1
366 item = l.currentItem()
367 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
368 l.editItem( item, c )
369 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
371 def address_label_clicked(self, item, column, l, column_addr, column_label):
372 if column==column_label and item.isSelected():
373 addr = unicode( item.text(column_addr) )
374 label = unicode( item.text(column_label) )
375 if label in self.wallet.aliases.keys():
377 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
378 l.editItem( item, column )
379 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
381 def address_label_changed(self, item, column, l, column_addr, column_label):
382 addr = unicode( item.text(column_addr) )
383 text = unicode( item.text(column_label) )
385 if text not in self.wallet.aliases.keys():
386 self.wallet.labels[addr] = text
388 print_error("Error: This is one of your aliases")
389 label = self.wallet.labels.get(addr,'')
390 item.setText(column_label, QString(label))
392 s = self.wallet.labels.get(addr)
393 if s: self.wallet.labels.pop(addr)
395 self.update_history_tab()
396 self.update_completions()
398 def update_history_tab(self):
399 self.history_list.clear()
401 for tx in self.wallet.get_tx_history():
402 tx_hash = tx['tx_hash']
404 conf = self.wallet.blocks - tx['height'] + 1
405 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
406 icon = QIcon(":icons/confirmed.png")
410 icon = QIcon(":icons/unconfirmed.png")
413 label = self.wallet.labels.get(tx_hash)
414 is_default_label = (label == '') or (label is None)
415 if is_default_label: label = tx['default_label']
417 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
418 item.setFont(2, QFont(MONOSPACE_FONT))
419 item.setFont(3, QFont(MONOSPACE_FONT))
420 item.setFont(4, QFont(MONOSPACE_FONT))
421 item.setToolTip(0, tx_hash)
423 item.setForeground(2, QBrush(QColor('grey')))
425 item.setIcon(0, icon)
426 self.history_list.insertTopLevelItem(0,item)
428 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
431 def create_send_tab(self):
436 grid.setColumnMinimumWidth(3,300)
437 grid.setColumnStretch(5,1)
439 self.payto_e = QLineEdit()
440 grid.addWidget(QLabel(_('Pay to')), 1, 0)
441 grid.addWidget(self.payto_e, 1, 1, 1, 3)
444 qrcode = qrscanner.scan_qr()
445 if 'address' in qrcode:
446 self.payto_e.setText(qrcode['address'])
447 if 'amount' in qrcode:
448 self.amount_e.setText(str(qrcode['amount']))
449 if 'label' in qrcode:
450 self.message_e.setText(qrcode['label'])
451 if 'message' in qrcode:
452 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
455 if qrscanner.is_available():
456 b = QPushButton(_("Scan QR code"))
457 b.clicked.connect(fill_from_qr)
458 grid.addWidget(b, 1, 5)
460 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)
462 completer = QCompleter()
463 completer.setCaseSensitivity(False)
464 self.payto_e.setCompleter(completer)
465 completer.setModel(self.completions)
467 self.message_e = QLineEdit()
468 grid.addWidget(QLabel(_('Description')), 2, 0)
469 grid.addWidget(self.message_e, 2, 1, 1, 3)
470 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)
472 self.amount_e = QLineEdit()
473 grid.addWidget(QLabel(_('Amount')), 3, 0)
474 grid.addWidget(self.amount_e, 3, 1, 1, 2)
475 grid.addWidget(HelpButton(
476 _('Amount to be sent.') + '\n\n' \
477 + _('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)
479 self.fee_e = QLineEdit()
480 grid.addWidget(QLabel(_('Fee')), 4, 0)
481 grid.addWidget(self.fee_e, 4, 1, 1, 2)
482 grid.addWidget(HelpButton(
483 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
484 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
485 + _('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)
487 b = EnterButton(_("Send"), self.do_send)
488 grid.addWidget(b, 6, 1)
490 b = EnterButton(_("Clear"),self.do_clear)
491 grid.addWidget(b, 6, 2)
493 self.payto_sig = QLabel('')
494 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
496 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
497 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
506 def entry_changed( is_fee ):
507 self.funds_error = False
508 amount = numbify(self.amount_e)
509 fee = numbify(self.fee_e)
510 if not is_fee: fee = None
513 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
515 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
518 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
521 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
522 self.funds_error = True
523 self.amount_e.setPalette(palette)
524 self.fee_e.setPalette(palette)
526 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
527 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
532 def update_completions(self):
534 for addr,label in self.wallet.labels.items():
535 if addr in self.wallet.addressbook:
536 l.append( label + ' <' + addr + '>')
537 l = l + self.wallet.aliases.keys()
539 self.completions.setStringList(l)
545 label = unicode( self.message_e.text() )
546 r = unicode( self.payto_e.text() )
550 m1 = re.match(ALIAS_REGEXP, r)
551 # label or alias, with address in brackets
552 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
555 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
559 to_address = m2.group(2)
563 if not self.wallet.is_valid(to_address):
564 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
568 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
570 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
573 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
575 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
578 if self.wallet.use_encryption:
579 password = self.password_dialog()
586 tx = self.wallet.mktx( to_address, amount, label, password, fee)
587 except BaseException, e:
588 self.show_message(str(e))
591 status, msg = self.wallet.sendtx( tx )
593 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
595 self.update_contacts_tab()
597 QMessageBox.warning(self, _('Error'), msg, _('OK'))
600 def set_url(self, url):
601 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
602 self.tabs.setCurrentIndex(1)
603 label = self.wallet.labels.get(payto)
604 m_addr = label + ' <'+ payto+'>' if label else payto
605 self.payto_e.setText(m_addr)
607 self.message_e.setText(message)
608 self.amount_e.setText(amount)
610 self.set_frozen(self.payto_e,True)
611 self.set_frozen(self.amount_e,True)
612 self.set_frozen(self.message_e,True)
613 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
615 self.payto_sig.setVisible(False)
618 self.payto_sig.setVisible(False)
619 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
621 self.set_frozen(e,False)
623 def set_frozen(self,entry,frozen):
625 entry.setReadOnly(True)
626 entry.setFrame(False)
628 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
629 entry.setPalette(palette)
631 entry.setReadOnly(False)
634 palette.setColor(entry.backgroundRole(), QColor('white'))
635 entry.setPalette(palette)
638 def toggle_freeze(self,addr):
640 if addr in self.wallet.frozen_addresses:
641 self.wallet.unfreeze(addr)
643 self.wallet.freeze(addr)
644 self.update_receive_tab()
646 def toggle_priority(self,addr):
648 if addr in self.wallet.prioritized_addresses:
649 self.wallet.unprioritize(addr)
651 self.wallet.prioritize(addr)
652 self.update_receive_tab()
655 def create_list_tab(self, headers):
656 "generic tab creation method"
657 l = MyTreeWidget(self)
658 l.setColumnCount( len(headers) )
659 l.setHeaderLabels( headers )
669 vbox.addWidget(buttons)
674 buttons.setLayout(hbox)
679 def create_receive_tab(self):
680 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
681 l.setContextMenuPolicy(Qt.CustomContextMenu)
682 l.customContextMenuRequested.connect(self.create_receive_menu)
683 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
684 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
685 self.receive_list = l
686 self.receive_buttons_hbox = hbox
690 def create_contacts_tab(self):
691 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
692 l.setContextMenuPolicy(Qt.CustomContextMenu)
693 l.customContextMenuRequested.connect(self.create_contact_menu)
694 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
695 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
696 self.contacts_list = l
697 self.contacts_buttons_hbox = hbox
698 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
703 def create_receive_menu(self, position):
704 # fixme: this function apparently has a side effect.
705 # if it is not called the menu pops up several times
706 #self.receive_list.selectedIndexes()
708 item = self.receive_list.itemAt(position)
710 addr = unicode(item.text(1))
712 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
713 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
714 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
715 if self.wallet.expert_mode:
716 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
717 menu.addAction(t, lambda: self.toggle_freeze(addr))
718 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
719 menu.addAction(t, lambda: self.toggle_priority(addr))
720 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
723 def payto(self, x, is_alias):
730 label = self.wallet.labels.get(addr)
731 m_addr = label + ' <' + addr + '>' if label else addr
732 self.tabs.setCurrentIndex(1)
733 self.payto_e.setText(m_addr)
734 self.amount_e.setFocus()
736 def delete_contact(self, x, is_alias):
737 if self.question("Do you want to remove %s from your list of contacts?"%x):
738 if not is_alias and x in self.wallet.addressbook:
739 self.wallet.addressbook.remove(x)
740 if x in self.wallet.labels.keys():
741 self.wallet.labels.pop(x)
742 elif is_alias and x in self.wallet.aliases:
743 self.wallet.aliases.pop(x)
744 self.update_history_tab()
745 self.update_contacts_tab()
746 self.update_completions()
748 def create_contact_menu(self, position):
749 # fixme: this function apparently has a side effect.
750 # if it is not called the menu pops up several times
751 #self.contacts_list.selectedIndexes()
753 item = self.contacts_list.itemAt(position)
755 addr = unicode(item.text(0))
756 label = unicode(item.text(1))
757 is_alias = label in self.wallet.aliases.keys()
758 x = label if is_alias else addr
760 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
761 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
762 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
764 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
766 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
767 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
768 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
771 def update_receive_tab(self):
772 l = self.receive_list
774 l.setColumnHidden(0,not self.wallet.expert_mode)
775 l.setColumnHidden(3,not self.wallet.expert_mode)
776 l.setColumnHidden(4,not self.wallet.expert_mode)
777 l.setColumnWidth(0, 50)
778 l.setColumnWidth(1, 310)
779 l.setColumnWidth(2, 250)
780 l.setColumnWidth(3, 130)
781 l.setColumnWidth(4, 10)
785 for address in self.wallet.all_addresses():
787 if self.wallet.is_change(address) and not self.wallet.expert_mode:
790 label = self.wallet.labels.get(address,'')
792 h = self.wallet.history.get(address,[])
794 if not item['is_input'] : n=n+1
798 if address in self.wallet.addresses:
800 if gap > self.wallet.gap_limit:
803 if address in self.wallet.addresses:
806 c, u = self.wallet.get_addr_balance(address)
807 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
808 flags = self.wallet.get_address_flags(address)
809 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
811 item.setFont(0, QFont(MONOSPACE_FONT))
812 item.setFont(1, QFont(MONOSPACE_FONT))
813 item.setFont(3, QFont(MONOSPACE_FONT))
814 if address in self.wallet.frozen_addresses:
815 item.setBackgroundColor(1, QColor('lightblue'))
816 elif address in self.wallet.prioritized_addresses:
817 item.setBackgroundColor(1, QColor('lightgreen'))
818 if is_red and address in self.wallet.addresses:
819 item.setBackgroundColor(1, QColor('red'))
820 l.addTopLevelItem(item)
822 # we use column 1 because column 0 may be hidden
823 l.setCurrentItem(l.topLevelItem(0),1)
825 def show_contact_details(self, m):
826 a = self.wallet.aliases.get(m)
828 if a[0] in self.wallet.authorities.keys():
829 s = self.wallet.authorities.get(a[0])
832 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
833 QMessageBox.information(self, 'Alias', msg, 'OK')
835 def update_contacts_tab(self):
837 l = self.contacts_list
839 l.setColumnHidden(2, not self.wallet.expert_mode)
840 l.setColumnWidth(0, 350)
841 l.setColumnWidth(1, 330)
842 l.setColumnWidth(2, 100)
845 for alias, v in self.wallet.aliases.items():
847 alias_targets.append(target)
848 item = QTreeWidgetItem( [ target, alias, '-'] )
849 item.setBackgroundColor(0, QColor('lightgray'))
850 l.addTopLevelItem(item)
852 for address in self.wallet.addressbook:
853 if address in alias_targets: continue
854 label = self.wallet.labels.get(address,'')
856 for item in self.wallet.tx_history.values():
857 if address in item['outputs'] : n=n+1
859 item = QTreeWidgetItem( [ address, label, tx] )
860 item.setFont(0, QFont(MONOSPACE_FONT))
861 l.addTopLevelItem(item)
863 l.setCurrentItem(l.topLevelItem(0))
865 def create_wall_tab(self):
866 self.textbox = textbox = QTextEdit(self)
867 textbox.setFont(QFont(MONOSPACE_FONT))
868 textbox.setReadOnly(True)
871 def create_status_bar(self):
873 sb.setFixedHeight(35)
875 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
876 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
878 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
879 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
880 sb.addPermanentWidget( self.status_button )
881 self.setStatusBar(sb)
883 def new_contact_dialog(self):
884 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
885 address = unicode(text)
887 if self.wallet.is_valid(address):
888 self.wallet.addressbook.append(address)
890 self.update_contacts_tab()
891 self.update_history_tab()
892 self.update_completions()
894 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
897 def show_seed_dialog(wallet, parent=None):
899 QMessageBox.information(parent, _('Message'),
900 _('No seed'), _('OK'))
903 if wallet.use_encryption:
904 password = parent.password_dialog()
911 seed = wallet.pw_decode(wallet.seed, password)
913 QMessageBox.warning(parent, _('Error'),
914 _('Incorrect Password'), _('OK'))
917 dialog = QDialog(None)
919 dialog.setWindowTitle(_("Seed"))
921 brainwallet = ' '.join(mnemonic.mn_encode(seed))
923 msg = _('<p>"%s"</p>'
924 "<p>If you memorise or write down these 12 words, you will always be able to recover your wallet.</p>"
925 "<p>This is called a 'BrainWallet'. The order of words is important. Case does not matter (capitals or lowercase).</p>") % brainwallet
926 main_text = QLabel(msg)
927 main_text.setWordWrap(True)
930 logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
937 copy_function = lambda: app.clipboard().setText(brainwallet)
938 copy_button = QPushButton(_("Copy to Clipboard"))
939 copy_button.clicked.connect(copy_function)
941 show_qr_function = lambda: ElectrumWindow.show_seed_qrcode(seed)
942 qr_button = QPushButton(_("View as QR Code"))
943 qr_button.clicked.connect(show_qr_function)
945 ok_button = QPushButton(_("OK"))
946 ok_button.clicked.connect(dialog.accept)
948 main_layout = QGridLayout()
949 main_layout.addWidget(logo, 0, 0)
950 main_layout.addWidget(main_text, 0, 1, 1, -1)
951 main_layout.addWidget(copy_button, 1, 1)
952 main_layout.addWidget(qr_button, 1, 2)
953 main_layout.addWidget(ok_button, 1, 3)
954 dialog.setLayout(main_layout)
959 def show_seed_qrcode(seed):
963 d.setWindowTitle(_("Seed"))
964 d.setMinimumSize(270, 300)
966 vbox.addWidget(QRCodeWidget(seed))
969 b = QPushButton(_("OK"))
971 b.clicked.connect(d.accept)
978 def show_address_qrcode(self,address):
979 if not address: return
982 d.setWindowTitle(address)
983 d.setMinimumSize(270, 350)
985 qrw = QRCodeWidget(address)
989 amount_e = QLineEdit()
990 hbox.addWidget(QLabel(_('Amount')))
991 hbox.addWidget(amount_e)
994 #hbox = QHBoxLayout()
995 #label_e = QLineEdit()
996 #hbox.addWidget(QLabel('Label'))
997 #hbox.addWidget(label_e)
998 #vbox.addLayout(hbox)
1000 def amount_changed():
1001 amount = numbify(amount_e)
1002 #label = str( label_e.getText() )
1003 if amount is not None:
1004 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
1006 qrw.set_addr( address )
1010 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
1011 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
1013 amount_e.textChanged.connect( amount_changed )
1015 hbox = QHBoxLayout()
1017 b = QPushButton(_("Save"))
1018 b.clicked.connect(do_save)
1020 b = QPushButton(_("Close"))
1022 b.clicked.connect(d.accept)
1024 vbox.addLayout(hbox)
1028 def question(self, msg):
1029 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1031 def show_message(self, msg):
1032 QMessageBox.information(self, _('Message'), msg, _('OK'))
1034 def password_dialog(self ):
1041 vbox = QVBoxLayout()
1042 msg = _('Please enter your password')
1043 vbox.addWidget(QLabel(msg))
1045 grid = QGridLayout()
1047 grid.addWidget(QLabel(_('Password')), 1, 0)
1048 grid.addWidget(pw, 1, 1)
1049 vbox.addLayout(grid)
1051 vbox.addLayout(ok_cancel_buttons(d))
1054 if not d.exec_(): return
1055 return unicode(pw.text())
1062 def change_password_dialog( wallet, parent=None ):
1065 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1073 new_pw = QLineEdit()
1074 new_pw.setEchoMode(2)
1075 conf_pw = QLineEdit()
1076 conf_pw.setEchoMode(2)
1078 vbox = QVBoxLayout()
1080 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')
1082 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1083 vbox.addWidget(QLabel(msg))
1085 grid = QGridLayout()
1088 if wallet.use_encryption:
1089 grid.addWidget(QLabel(_('Password')), 1, 0)
1090 grid.addWidget(pw, 1, 1)
1092 grid.addWidget(QLabel(_('New Password')), 2, 0)
1093 grid.addWidget(new_pw, 2, 1)
1095 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1096 grid.addWidget(conf_pw, 3, 1)
1097 vbox.addLayout(grid)
1099 vbox.addLayout(ok_cancel_buttons(d))
1102 if not d.exec_(): return
1104 password = unicode(pw.text()) if wallet.use_encryption else None
1105 new_password = unicode(new_pw.text())
1106 new_password2 = unicode(conf_pw.text())
1109 seed = wallet.pw_decode( wallet.seed, password)
1111 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1114 if new_password != new_password2:
1115 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1118 wallet.update_password(seed, password, new_password)
1121 def seed_dialog(wallet, parent=None):
1125 vbox = QVBoxLayout()
1126 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1127 vbox.addWidget(QLabel(msg))
1129 grid = QGridLayout()
1132 seed_e = QLineEdit()
1133 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1134 grid.addWidget(seed_e, 1, 1)
1138 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1139 grid.addWidget(gap_e, 2, 1)
1140 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1141 vbox.addLayout(grid)
1143 vbox.addLayout(ok_cancel_buttons(d))
1146 if not d.exec_(): return
1149 gap = int(unicode(gap_e.text()))
1151 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1155 seed = unicode(seed_e.text())
1158 print_error("Warning: Not hex, trying decode")
1160 seed = mnemonic.mn_decode( seed.split(' ') )
1162 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1165 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1168 wallet.seed = str(seed)
1169 #print repr(wallet.seed)
1170 wallet.gap_limit = gap
1174 def set_expert_mode(self, b):
1175 self.wallet.expert_mode = b
1177 self.update_receive_tab()
1178 self.update_contacts_tab()
1179 # if self.wallet.seed:
1180 # self.nochange_cb.setHidden(not self.wallet.expert_mode)
1183 def settings_dialog(self):
1186 vbox = QVBoxLayout()
1187 msg = _('Here are the settings of your wallet.') + '\n'\
1188 + _('For more explanations, click on the help buttons next to each field.')
1191 label.setFixedWidth(250)
1192 label.setWordWrap(True)
1193 label.setAlignment(Qt.AlignJustify)
1194 vbox.addWidget(label)
1196 grid = QGridLayout()
1198 vbox.addLayout(grid)
1201 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1202 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1203 grid.addWidget(fee_e, 2, 1)
1204 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1205 + _('Recommended value') + ': 0.001'
1206 grid.addWidget(HelpButton(msg), 2, 2)
1207 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1210 nz_e.setText("%d"% self.wallet.num_zeros)
1211 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1212 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1213 grid.addWidget(HelpButton(msg), 3, 2)
1214 grid.addWidget(nz_e, 3, 1)
1215 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1217 cb = QCheckBox(_('Expert mode'))
1218 grid.addWidget(cb, 4, 0)
1219 cb.setChecked(self.wallet.expert_mode)
1221 if self.wallet.expert_mode:
1223 usechange_cb = QCheckBox(_('Use change addresses'))
1224 grid.addWidget(usechange_cb, 5, 0)
1225 usechange_cb.setChecked(self.wallet.use_change)
1226 grid.addWidget(HelpButton(_('Using a change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1228 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1229 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1230 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1231 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1232 + _('Warning') + ': ' \
1233 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1234 + _('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'
1236 gap_e.setText("%d"% self.wallet.gap_limit)
1237 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1238 grid.addWidget(gap_e, 6, 1)
1239 grid.addWidget(HelpButton(msg), 6, 2)
1240 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1243 vbox.addLayout(ok_cancel_buttons(d))
1247 if not d.exec_(): return
1249 fee = unicode(fee_e.text())
1251 fee = int( 100000000 * Decimal(fee) )
1253 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1256 if self.wallet.fee != fee:
1257 self.wallet.fee = fee
1260 nz = unicode(nz_e.text())
1265 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1268 if self.wallet.num_zeros != nz:
1269 self.wallet.num_zeros = nz
1270 self.update_history_tab()
1271 self.update_receive_tab()
1274 if self.wallet.expert_mode:
1276 self.wallet.use_change = usechange_cb.isChecked()
1279 n = int(gap_e.text())
1281 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1283 if self.wallet.gap_limit != n:
1284 r = self.wallet.change_gap_limit(n)
1286 self.update_receive_tab()
1288 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1290 self.set_expert_mode(cb.isChecked())
1294 def network_dialog(wallet, parent=None):
1295 interface = wallet.interface
1297 if interface.is_connected:
1298 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1300 status = _("Not connected")
1301 server = wallet.server
1304 status = _("Please choose a server.")
1305 server = random.choice( DEFAULT_SERVERS )
1307 if not wallet.interface.servers:
1309 for x in DEFAULT_SERVERS:
1310 h,port,protocol = x.split(':')
1311 servers_list.append( (h,[(protocol,port)] ) )
1313 servers_list = wallet.interface.servers
1316 for item in servers_list:
1320 protocol, port = item2
1326 d.setWindowTitle(_('Server'))
1327 d.setMinimumSize(375, 20)
1329 vbox = QVBoxLayout()
1332 hbox = QHBoxLayout()
1334 l.setPixmap(QPixmap(":icons/network.png"))
1336 hbox.addWidget(QLabel(status))
1338 vbox.addLayout(hbox)
1340 hbox = QHBoxLayout()
1341 host_line = QLineEdit()
1342 host_line.setText(server)
1343 hbox.addWidget(QLabel(_('Connect to') + ':'))
1344 hbox.addWidget(host_line)
1345 vbox.addLayout(hbox)
1347 hbox = QHBoxLayout()
1349 buttonGroup = QGroupBox(_("Protocol"))
1350 radio1 = QRadioButton("tcp", buttonGroup)
1351 radio2 = QRadioButton("http", buttonGroup)
1354 return unicode(host_line.text()).split(':')
1356 def set_button(protocol):
1358 radio1.setChecked(1)
1359 elif protocol == 'h':
1360 radio2.setChecked(1)
1362 def set_protocol(protocol):
1363 host = current_line()[0]
1365 if protocol not in pp.keys():
1366 protocol = pp.keys()[0]
1367 set_button(protocol)
1369 host_line.setText( host + ':' + port + ':' + protocol)
1371 radio1.clicked.connect(lambda x: set_protocol('t') )
1372 radio2.clicked.connect(lambda x: set_protocol('h') )
1374 set_button(current_line()[2])
1376 hbox.addWidget(QLabel(_('Protocol')+':'))
1377 hbox.addWidget(radio1)
1378 hbox.addWidget(radio2)
1380 vbox.addLayout(hbox)
1382 if wallet.interface.servers:
1383 label = _('Active Servers')
1385 label = _('Default Servers')
1387 servers_list_widget = QTreeWidget(parent)
1388 servers_list_widget.setHeaderLabels( [ label ] )
1389 servers_list_widget.setMaximumHeight(150)
1390 for host in plist.keys():
1391 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1394 host = unicode(x.text(0))
1396 if 't' in pp.keys():
1399 protocol = pp.keys()[0]
1401 host_line.setText( host + ':' + port + ':' + protocol)
1402 set_button(protocol)
1404 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1405 vbox.addWidget(servers_list_widget)
1407 vbox.addLayout(ok_cancel_buttons(d))
1410 if not d.exec_(): return
1411 server = unicode( host_line.text() )
1414 wallet.set_server(server)
1416 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1428 def __init__(self, wallet, app=None):
1429 self.wallet = wallet
1431 self.app = QApplication(sys.argv)
1433 def server_list_changed(self):
1436 def waiting_dialog(self):
1442 w.setWindowTitle('Electrum')
1444 vbox = QVBoxLayout()
1449 if self.wallet.up_to_date:
1452 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1453 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1455 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1456 self.wallet.interface.poke()
1461 def restore_or_create(self):
1463 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1464 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1465 if r==2: return False
1467 is_recovery = (r==1)
1468 wallet = self.wallet
1469 # ask for the server.
1470 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1473 wallet.new_seed(None)
1474 wallet.init_mpk( wallet.seed )
1475 wallet.up_to_date_event.clear()
1476 wallet.up_to_date = False
1477 self.waiting_dialog()
1478 # run a dialog indicating the seed, ask the user to remember it
1479 ElectrumWindow.show_seed_dialog(wallet)
1481 ElectrumWindow.change_password_dialog(wallet)
1483 # ask for seed and gap.
1484 if not ElectrumWindow.seed_dialog( wallet ): return False
1485 wallet.init_mpk( wallet.seed )
1486 wallet.up_to_date_event.clear()
1487 wallet.up_to_date = False
1488 self.waiting_dialog()
1489 if wallet.is_found():
1490 # history and addressbook
1491 wallet.update_tx_history()
1492 wallet.fill_addressbook()
1493 print "Recovery successful"
1496 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1504 w = ElectrumWindow(self.wallet)
1505 if url: w.set_url(url)