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.setFixedWidth(20)
84 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
87 class EnterButton(QPushButton):
88 def __init__(self, text, func):
89 QPushButton.__init__(self, text)
91 self.clicked.connect(func)
93 def keyPressEvent(self, e):
94 if e.key() == QtCore.Qt.Key_Return:
97 class MyTreeWidget(QTreeWidget):
98 def __init__(self, parent):
99 QTreeWidget.__init__(self, parent)
102 for i in range(0,self.viewport().height()/5):
103 if self.itemAt(QPoint(0,i*5)) == item:
107 for j in range(0,30):
108 if self.itemAt(QPoint(0,i*5 + j)) != item:
110 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
112 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
117 class StatusBarButton(QPushButton):
118 def __init__(self, icon, tooltip, func):
119 QPushButton.__init__(self, icon, '')
120 self.setToolTip(tooltip)
122 self.setMaximumWidth(25)
123 self.clicked.connect(func)
126 def keyPressEvent(self, e):
127 if e.key() == QtCore.Qt.Key_Return:
131 class QRCodeWidget(QWidget):
133 def __init__(self, addr):
134 super(QRCodeWidget, self).__init__()
135 self.setGeometry(300, 300, 350, 350)
138 def set_addr(self, addr):
140 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
141 self.qr.addData(addr)
144 def paintEvent(self, e):
145 qp = QtGui.QPainter()
148 size = self.qr.getModuleCount()*boxsize
149 k = self.qr.getModuleCount()
150 black = QColor(0, 0, 0, 255)
151 white = QColor(255, 255, 255, 255)
154 if self.qr.isDark(r, c):
160 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
165 def ok_cancel_buttons(dialog):
168 b = QPushButton("OK")
170 b.clicked.connect(dialog.accept)
171 b = QPushButton("Cancel")
173 b.clicked.connect(dialog.reject)
177 class ElectrumWindow(QMainWindow):
179 def __init__(self, wallet):
180 QMainWindow.__init__(self)
182 self.wallet.gui_callback = self.update_callback
184 self.funds_error = False
185 self.completions = QStringListModel()
187 self.tabs = tabs = QTabWidget(self)
188 tabs.addTab(self.create_history_tab(), _('History') )
190 tabs.addTab(self.create_send_tab(), _('Send') )
191 tabs.addTab(self.create_receive_tab(), _('Receive') )
192 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
193 tabs.addTab(self.create_wall_tab(), _('Wall') )
194 tabs.setMinimumSize(600, 400)
195 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
196 self.setCentralWidget(tabs)
197 self.create_status_bar()
198 self.setGeometry(100,100,840,400)
199 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
200 if not self.wallet.seed: title += ' [seedless]'
201 self.setWindowTitle( title )
204 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
205 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
206 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
207 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
209 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
210 self.history_list.setFocus(True)
213 def connect_slots(self, sender):
215 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
216 self.previous_payto_e=''
218 def check_recipient(self):
219 if self.payto_e.hasFocus():
221 r = unicode( self.payto_e.text() )
222 if r != self.previous_payto_e:
223 self.previous_payto_e = r
225 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
227 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
231 s = r + ' <' + to_address + '>'
232 self.payto_e.setText(s)
235 def update_callback(self):
236 self.emit(QtCore.SIGNAL('updatesignal'))
238 def update_wallet(self):
239 if self.wallet.interface and self.wallet.interface.is_connected:
240 if self.wallet.blocks == -1:
241 text = _( "Connecting..." )
242 icon = QIcon(":icons/status_disconnected.png")
243 elif self.wallet.blocks == 0:
244 text = _( "Server not ready" )
245 icon = QIcon(":icons/status_disconnected.png")
246 elif not self.wallet.up_to_date:
247 text = _( "Synchronizing..." )
248 icon = QIcon(":icons/status_waiting.png")
250 c, u = self.wallet.get_balance()
251 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
252 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
253 icon = QIcon(":icons/status_connected.png")
255 text = _( "Not connected" )
256 icon = QIcon(":icons/status_disconnected.png")
259 text = _( "Not enough funds" )
261 self.statusBar().showMessage(text)
262 self.status_button.setIcon( icon )
264 if self.wallet.up_to_date:
265 self.textbox.setText( self.wallet.banner )
266 self.update_history_tab()
267 self.update_receive_tab()
268 self.update_contacts_tab()
269 self.update_completions()
272 def create_history_tab(self):
273 self.history_list = l = MyTreeWidget(self)
275 l.setColumnWidth(0, 40)
276 l.setColumnWidth(1, 140)
277 l.setColumnWidth(2, 350)
278 l.setColumnWidth(3, 140)
279 l.setColumnWidth(4, 140)
280 l.setHeaderLabels( [ '', _( 'Date' ), _( 'Description' ) , _('Amount'), _('Balance')] )
281 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
282 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
283 l.setContextMenuPolicy(Qt.CustomContextMenu)
284 l.customContextMenuRequested.connect(self.create_history_menu)
287 def create_history_menu(self, position):
288 self.history_list.selectedIndexes()
289 item = self.history_list.currentItem()
291 tx_hash = str(item.toolTip(0))
293 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
294 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
295 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
296 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
298 def tx_details(self, tx_hash):
299 tx = self.wallet.tx_history.get(tx_hash)
302 conf = self.wallet.blocks - tx['height'] + 1
303 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
308 tx_details = _("Transaction Details") +"\n\n" \
309 + "Transaction ID:\n" + tx_hash + "\n\n" \
310 + "Status: %d confirmations\n\n"%conf \
311 + "Date: %s\n\n"%time_str \
312 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
313 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
315 r = self.wallet.receipts.get(tx_hash)
317 tx_details += "\n_______________________________________" \
318 + '\n\nSigned URI: ' + r[2] \
319 + "\n\nSigned by: " + r[0] \
320 + '\n\nSignature: ' + r[1]
322 QMessageBox.information(self, 'Details', tx_details, 'OK')
325 def tx_label_clicked(self, item, column):
326 if column==2 and item.isSelected():
327 tx_hash = str(item.toolTip(0))
329 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
330 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
331 self.history_list.editItem( item, column )
332 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
335 def tx_label_changed(self, item, column):
339 tx_hash = str(item.toolTip(0))
340 tx = self.wallet.tx_history.get(tx_hash)
341 s = self.wallet.labels.get(tx_hash)
342 text = unicode( item.text(2) )
344 self.wallet.labels[tx_hash] = text
345 item.setForeground(2, QBrush(QColor('black')))
347 if s: self.wallet.labels.pop(tx_hash)
348 text = tx['default_label']
349 item.setText(2, text)
350 item.setForeground(2, QBrush(QColor('gray')))
353 def edit_label(self, is_recv):
354 l = self.receive_list if is_recv else self.contacts_list
355 c = 2 if is_recv else 1
356 item = l.currentItem()
357 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
358 l.editItem( item, c )
359 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
361 def address_label_clicked(self, item, column, l, column_addr, column_label):
362 if column==column_label and item.isSelected():
363 addr = unicode( item.text(column_addr) )
364 label = unicode( item.text(column_label) )
365 if label in self.wallet.aliases.keys():
367 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
368 l.editItem( item, column )
369 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
371 def address_label_changed(self, item, column, l, column_addr, column_label):
372 addr = unicode( item.text(column_addr) )
373 text = unicode( item.text(column_label) )
375 if text not in self.wallet.aliases.keys():
376 self.wallet.labels[addr] = text
378 print "error: this is one of your aliases"
379 label = self.wallet.labels.get(addr,'')
380 item.setText(column_label, QString(label))
382 s = self.wallet.labels.get(addr)
383 if s: self.wallet.labels.pop(addr)
385 self.update_history_tab()
386 self.update_completions()
388 def update_history_tab(self):
389 self.history_list.clear()
391 for tx in self.wallet.get_tx_history():
392 tx_hash = tx['tx_hash']
394 conf = self.wallet.blocks - tx['height'] + 1
395 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
396 icon = QIcon(":icons/confirmed.png")
400 icon = QIcon(":icons/unconfirmed.png")
403 label = self.wallet.labels.get(tx_hash)
404 is_default_label = (label == '') or (label is None)
405 if is_default_label: label = tx['default_label']
407 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
408 item.setFont(2, QFont(MONOSPACE_FONT))
409 item.setFont(3, QFont(MONOSPACE_FONT))
410 item.setFont(4, QFont(MONOSPACE_FONT))
411 item.setToolTip(0, tx_hash)
413 item.setForeground(2, QBrush(QColor('grey')))
415 item.setIcon(0, icon)
416 self.history_list.insertTopLevelItem(0,item)
418 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
421 def create_send_tab(self):
426 grid.setColumnMinimumWidth(3,300)
427 grid.setColumnStretch(4,1)
429 self.payto_e = QLineEdit()
430 grid.addWidget(QLabel(_('Pay to')), 1, 0)
431 grid.addWidget(self.payto_e, 1, 1, 1, 3)
433 completer = QCompleter()
434 completer.setCaseSensitivity(False)
435 self.payto_e.setCompleter(completer)
436 completer.setModel(self.completions)
438 self.message_e = QLineEdit()
439 grid.addWidget(QLabel(_('Description')), 2, 0)
440 grid.addWidget(self.message_e, 2, 1, 1, 3)
442 self.amount_e = QLineEdit()
443 grid.addWidget(QLabel(_('Amount')), 3, 0)
444 grid.addWidget(self.amount_e, 3, 1, 1, 2)
446 self.nochange_cb = QCheckBox('Do not create change address')
447 grid.addWidget(self.nochange_cb,3,3)
448 self.nochange_cb.setChecked(False)
449 self.nochange_cb.setHidden(not self.wallet.expert_mode)
451 self.fee_e = QLineEdit()
452 grid.addWidget(QLabel(_('Fee')), 4, 0)
453 grid.addWidget(self.fee_e, 4, 1, 1, 2)
455 b = EnterButton(_("Send"), self.do_send)
456 grid.addWidget(b, 5, 1)
458 b = EnterButton(_("Clear"),self.do_clear)
459 grid.addWidget(b, 5, 2)
461 self.payto_sig = QLabel('')
462 grid.addWidget(self.payto_sig, 6, 0, 1, 4)
464 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
465 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
475 def entry_changed( is_fee ):
476 self.funds_error = False
477 amount = numbify(self.amount_e)
478 fee = numbify(self.fee_e)
479 if not is_fee: fee = None
482 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
484 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
487 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
490 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
491 self.funds_error = True
492 self.amount_e.setPalette(palette)
493 self.fee_e.setPalette(palette)
495 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
496 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
501 def update_completions(self):
503 for addr,label in self.wallet.labels.items():
504 if addr in self.wallet.addressbook:
505 l.append( label + ' <' + addr + '>')
506 l = l + self.wallet.aliases.keys()
508 self.completions.setStringList(l)
514 label = unicode( self.message_e.text() )
515 r = unicode( self.payto_e.text() )
519 m1 = re.match(ALIAS_REGEXP, r)
520 # label or alias, with address in brackets
521 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
524 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
528 to_address = m2.group(2)
532 if not self.wallet.is_valid(to_address):
533 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
537 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
539 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
542 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
544 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
547 if self.wallet.use_encryption:
548 password = self.password_dialog()
554 if self.nochange_cb.isChecked():
555 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
556 change_addr = inputs[0][0]
557 print "sending change to", change_addr
562 tx = self.wallet.mktx( to_address, amount, label, password, fee, change_addr )
563 except BaseException, e:
564 self.show_message(str(e))
567 status, msg = self.wallet.sendtx( tx )
569 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
571 self.update_contacts_tab()
573 QMessageBox.warning(self, _('Error'), msg, _('OK'))
576 def set_url(self, url):
577 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
578 self.tabs.setCurrentIndex(1)
579 label = self.wallet.labels.get(payto)
580 m_addr = label + ' <'+ payto+'>' if label else payto
581 self.payto_e.setText(m_addr)
583 self.message_e.setText(message)
584 self.amount_e.setText(amount)
586 self.set_frozen(self.payto_e,True)
587 self.set_frozen(self.amount_e,True)
588 self.set_frozen(self.message_e,True)
589 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
591 self.payto_sig.setVisible(False)
594 self.payto_sig.setVisible(False)
595 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
597 self.set_frozen(e,False)
599 def set_frozen(self,entry,frozen):
601 entry.setReadOnly(True)
602 entry.setFrame(False)
604 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
605 entry.setPalette(palette)
607 entry.setReadOnly(False)
610 palette.setColor(entry.backgroundRole(), QColor('white'))
611 entry.setPalette(palette)
614 def toggle_freeze(self,addr):
616 if addr in self.wallet.frozen_addresses:
617 self.wallet.unfreeze(addr)
619 self.wallet.freeze(addr)
620 self.update_receive_tab()
622 def toggle_priority(self,addr):
624 if addr in self.wallet.prioritized_addresses:
625 self.wallet.unprioritize(addr)
627 self.wallet.prioritize(addr)
628 self.update_receive_tab()
631 def create_list_tab(self, headers):
632 "generic tab creation method"
633 l = MyTreeWidget(self)
634 l.setColumnCount( len(headers) )
635 l.setHeaderLabels( headers )
645 vbox.addWidget(buttons)
650 buttons.setLayout(hbox)
655 def create_receive_tab(self):
656 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
657 l.setContextMenuPolicy(Qt.CustomContextMenu)
658 l.customContextMenuRequested.connect(self.create_receive_menu)
659 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
660 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
661 self.receive_list = l
662 self.receive_buttons_hbox = hbox
666 def create_contacts_tab(self):
667 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
668 l.setContextMenuPolicy(Qt.CustomContextMenu)
669 l.customContextMenuRequested.connect(self.create_contact_menu)
670 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
671 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
672 self.contacts_list = l
673 self.contacts_buttons_hbox = hbox
674 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
679 def create_receive_menu(self, position):
680 # fixme: this function apparently has a side effect.
681 # if it is not called the menu pops up several times
682 #self.receive_list.selectedIndexes()
684 item = self.receive_list.itemAt(position)
686 addr = unicode(item.text(1))
688 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
689 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
690 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
691 if self.wallet.expert_mode:
692 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
693 menu.addAction(t, lambda: self.toggle_freeze(addr))
694 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
695 menu.addAction(t, lambda: self.toggle_priority(addr))
696 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
699 def payto(self, x, is_alias):
706 label = self.wallet.labels.get(addr)
707 m_addr = label + ' <' + addr + '>' if label else addr
708 self.tabs.setCurrentIndex(1)
709 self.payto_e.setText(m_addr)
710 self.amount_e.setFocus()
712 def delete_contact(self, x, is_alias):
713 if self.question("Do you want to remove %s from your list of contacts?"%x):
714 if not is_alias and x in self.wallet.addressbook:
715 self.wallet.addressbook.remove(x)
716 if x in self.wallet.labels.keys():
717 self.wallet.labels.pop(x)
718 elif is_alias and x in self.wallet.aliases:
719 self.wallet.aliases.pop(x)
720 self.update_history_tab()
721 self.update_contacts_tab()
722 self.update_completions()
724 def create_contact_menu(self, position):
725 # fixme: this function apparently has a side effect.
726 # if it is not called the menu pops up several times
727 #self.contacts_list.selectedIndexes()
729 item = self.contacts_list.itemAt(position)
731 addr = unicode(item.text(0))
732 label = unicode(item.text(1))
733 is_alias = label in self.wallet.aliases.keys()
734 x = label if is_alias else addr
736 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
737 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
738 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
740 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
742 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
743 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
744 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
747 def update_receive_tab(self):
748 l = self.receive_list
750 l.setColumnHidden(0,not self.wallet.expert_mode)
751 l.setColumnHidden(3,not self.wallet.expert_mode)
752 l.setColumnHidden(4,not self.wallet.expert_mode)
753 l.setColumnWidth(0, 50)
754 l.setColumnWidth(1, 310)
755 l.setColumnWidth(2, 250)
756 l.setColumnWidth(3, 130)
757 l.setColumnWidth(4, 10)
761 for address in self.wallet.all_addresses():
763 if self.wallet.is_change(address) and not self.wallet.expert_mode:
766 label = self.wallet.labels.get(address,'')
768 h = self.wallet.history.get(address,[])
770 if not item['is_input'] : n=n+1
774 if address in self.wallet.addresses:
776 if gap > self.wallet.gap_limit:
780 if address in self.wallet.addresses:
783 c, u = self.wallet.get_addr_balance(address)
784 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
785 flags = self.wallet.get_address_flags(address)
786 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
788 item.setFont(0, QFont(MONOSPACE_FONT))
789 item.setFont(1, QFont(MONOSPACE_FONT))
790 item.setFont(3, QFont(MONOSPACE_FONT))
791 if address in self.wallet.frozen_addresses:
792 item.setBackgroundColor(1, QColor('lightblue'))
793 elif address in self.wallet.prioritized_addresses:
794 item.setBackgroundColor(1, QColor('lightgreen'))
795 if is_red and address in self.wallet.addresses:
796 item.setBackgroundColor(1, QColor('red'))
797 l.addTopLevelItem(item)
799 # we use column 1 because column 0 may be hidden
800 l.setCurrentItem(l.topLevelItem(0),1)
802 def show_contact_details(self, m):
803 a = self.wallet.aliases.get(m)
805 if a[0] in self.wallet.authorities.keys():
806 s = self.wallet.authorities.get(a[0])
809 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
810 QMessageBox.information(self, 'Alias', msg, 'OK')
812 def update_contacts_tab(self):
814 l = self.contacts_list
816 l.setColumnHidden(2, not self.wallet.expert_mode)
817 l.setColumnWidth(0, 350)
818 l.setColumnWidth(1, 330)
819 l.setColumnWidth(2, 100)
822 for alias, v in self.wallet.aliases.items():
824 alias_targets.append(target)
825 item = QTreeWidgetItem( [ target, alias, '-'] )
826 item.setBackgroundColor(0, QColor('lightgray'))
827 l.addTopLevelItem(item)
829 for address in self.wallet.addressbook:
830 if address in alias_targets: continue
831 label = self.wallet.labels.get(address,'')
833 for item in self.wallet.tx_history.values():
834 if address in item['outputs'] : n=n+1
835 tx = "None" if n==0 else "%d"%n
836 item = QTreeWidgetItem( [ address, label, tx] )
837 item.setFont(0, QFont(MONOSPACE_FONT))
838 l.addTopLevelItem(item)
840 l.setCurrentItem(l.topLevelItem(0))
842 def create_wall_tab(self):
843 self.textbox = textbox = QTextEdit(self)
844 textbox.setFont(QFont(MONOSPACE_FONT))
845 textbox.setReadOnly(True)
848 def create_status_bar(self):
850 sb.setFixedHeight(35)
852 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
853 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
855 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
856 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
857 sb.addPermanentWidget( self.status_button )
858 self.setStatusBar(sb)
860 def new_contact_dialog(self):
861 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
862 address = unicode(text)
864 if self.wallet.is_valid(address):
865 self.wallet.addressbook.append(address)
867 self.update_contacts_tab()
868 self.update_history_tab()
869 self.update_completions()
871 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
874 def show_seed_dialog(wallet, parent=None):
877 QMessageBox.information(parent, _('Message'), _('No seed'), _('OK'))
880 if wallet.use_encryption:
881 password = parent.password_dialog()
882 if not password: return
887 seed = wallet.pw_decode( wallet.seed, password)
889 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
892 msg = _("Your wallet generation seed is") + ":\n\n" + seed + "\n\n"\
893 + _("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") + "\n\n" \
894 + _("Equivalently, your wallet seed can be stored and recovered with the following mnemonic code") + ":\n\n\"" \
895 + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n"
899 d.setWindowTitle(_("Seed"))
900 d.setMinimumSize(400, 270)
904 vbox2 = QVBoxLayout()
906 l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
909 hbox.addLayout(vbox2)
910 hbox.addWidget(QLabel(msg))
922 b = QPushButton(_("Copy to Clipboard"))
923 b.clicked.connect(lambda: app.clipboard().setText(seed + ' "' + ' '.join(mnemonic.mn_encode(seed))+'"'))
925 b = QPushButton(_("View as QR Code"))
926 b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed))
929 b = QPushButton(_("OK"))
930 b.clicked.connect(d.accept)
937 def show_seed_qrcode(seed):
941 d.setWindowTitle(_("Seed"))
942 d.setMinimumSize(270, 300)
944 vbox.addWidget(QRCodeWidget(seed))
947 b = QPushButton(_("OK"))
949 b.clicked.connect(d.accept)
956 def show_address_qrcode(self,address):
957 if not address: return
960 d.setWindowTitle(address)
961 d.setMinimumSize(270, 350)
963 qrw = QRCodeWidget(address)
967 amount_e = QLineEdit()
968 hbox.addWidget(QLabel(_('Amount')))
969 hbox.addWidget(amount_e)
972 #hbox = QHBoxLayout()
973 #label_e = QLineEdit()
974 #hbox.addWidget(QLabel('Label'))
975 #hbox.addWidget(label_e)
976 #vbox.addLayout(hbox)
978 def amount_changed():
979 amount = numbify(amount_e)
980 #label = str( label_e.getText() )
981 if amount is not None:
982 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
984 qrw.set_addr( address )
988 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
989 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
991 amount_e.textChanged.connect( amount_changed )
995 b = QPushButton(_("Save"))
996 b.clicked.connect(do_save)
998 b = QPushButton(_("Close"))
1000 b.clicked.connect(d.accept)
1002 vbox.addLayout(hbox)
1006 def question(self, msg):
1007 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1009 def show_message(self, msg):
1010 QMessageBox.information(self, _('Message'), msg, _('OK'))
1012 def password_dialog(self ):
1019 vbox = QVBoxLayout()
1020 msg = _('Please enter your password')
1021 vbox.addWidget(QLabel(msg))
1023 grid = QGridLayout()
1025 grid.addWidget(QLabel(_('Password')), 1, 0)
1026 grid.addWidget(pw, 1, 1)
1027 vbox.addLayout(grid)
1029 vbox.addLayout(ok_cancel_buttons(d))
1032 if not d.exec_(): return
1033 return unicode(pw.text())
1040 def change_password_dialog( wallet, parent=None ):
1043 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1051 new_pw = QLineEdit()
1052 new_pw.setEchoMode(2)
1053 conf_pw = QLineEdit()
1054 conf_pw.setEchoMode(2)
1056 vbox = QVBoxLayout()
1058 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')
1060 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1061 vbox.addWidget(QLabel(msg))
1063 grid = QGridLayout()
1066 if wallet.use_encryption:
1067 grid.addWidget(QLabel(_('Password')), 1, 0)
1068 grid.addWidget(pw, 1, 1)
1070 grid.addWidget(QLabel(_('New Password')), 2, 0)
1071 grid.addWidget(new_pw, 2, 1)
1073 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1074 grid.addWidget(conf_pw, 3, 1)
1075 vbox.addLayout(grid)
1077 vbox.addLayout(ok_cancel_buttons(d))
1080 if not d.exec_(): return
1082 password = unicode(pw.text()) if wallet.use_encryption else None
1083 new_password = unicode(new_pw.text())
1084 new_password2 = unicode(conf_pw.text())
1087 seed = wallet.pw_decode( wallet.seed, password)
1089 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1092 if new_password != new_password2:
1093 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1096 wallet.update_password(seed, password, new_password)
1099 def seed_dialog(wallet, parent=None):
1103 vbox = QVBoxLayout()
1104 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1105 vbox.addWidget(QLabel(msg))
1107 grid = QGridLayout()
1110 seed_e = QLineEdit()
1111 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1112 grid.addWidget(seed_e, 1, 1)
1116 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1117 grid.addWidget(gap_e, 2, 1)
1118 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1119 vbox.addLayout(grid)
1121 vbox.addLayout(ok_cancel_buttons(d))
1124 if not d.exec_(): return
1127 gap = int(unicode(gap_e.text()))
1129 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1133 seed = unicode(seed_e.text())
1136 print "not hex, trying decode"
1138 seed = mnemonic.mn_decode( seed.split(' ') )
1140 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1143 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1146 wallet.seed = str(seed)
1147 #print repr(wallet.seed)
1148 wallet.gap_limit = gap
1152 def set_expert_mode(self, b):
1153 self.wallet.expert_mode = b
1155 self.update_receive_tab()
1156 self.update_contacts_tab()
1157 self.nochange_cb.setHidden(not self.wallet.expert_mode)
1160 def settings_dialog(self):
1163 vbox = QVBoxLayout()
1164 msg = _('Here are the settings of your wallet.') + '\n'\
1165 + _('For more explanations, click on the help buttons next to each field.')
1168 label.setFixedWidth(250)
1169 label.setWordWrap(True)
1170 label.setAlignment(Qt.AlignJustify)
1171 vbox.addWidget(label)
1173 grid = QGridLayout()
1175 vbox.addLayout(grid)
1178 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1179 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1180 grid.addWidget(fee_e, 2, 1)
1181 grid.addWidget(HelpButton('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee. Recommended value: 0.001'), 2, 2)
1182 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1185 nz_e.setText("%d"% self.wallet.num_zeros)
1186 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1187 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)
1188 grid.addWidget(nz_e, 3, 1)
1189 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1191 if self.wallet.expert_mode:
1192 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1193 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1194 + _('Your current gap limit is: ') + '%d'%self.wallet.gap_limit + '\n' \
1195 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1196 + _('Warning:') + ' ' \
1197 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1198 + _('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'
1200 gap_e.setText("%d"% self.wallet.gap_limit)
1201 grid.addWidget(QLabel(_('Gap limit')), 4, 0)
1202 grid.addWidget(gap_e, 4, 1)
1203 grid.addWidget(HelpButton(msg), 4, 2)
1204 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1206 cb = QCheckBox('Expert mode')
1207 grid.addWidget(cb, 5, 0)
1208 cb.setChecked(self.wallet.expert_mode)
1210 vbox.addLayout(ok_cancel_buttons(d))
1214 if not d.exec_(): return
1216 fee = unicode(fee_e.text())
1218 fee = int( 100000000 * Decimal(fee) )
1220 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1223 if self.wallet.fee != fee:
1224 self.wallet.fee = fee
1227 nz = unicode(nz_e.text())
1232 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1235 if self.wallet.num_zeros != nz:
1236 self.wallet.num_zeros = nz
1237 self.update_history_tab()
1238 self.update_receive_tab()
1241 if self.wallet.expert_mode:
1243 n = int(gap_e.text())
1245 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1247 if self.wallet.gap_limit != n:
1248 r = self.wallet.change_gap_limit(n)
1250 self.update_receive_tab()
1252 QMessageBox.warning(self, _('Error'), _('Invalid Value'), _('OK'))
1254 self.set_expert_mode(cb.isChecked())
1258 def network_dialog(wallet, parent=None):
1259 interface = wallet.interface
1261 if interface.is_connected:
1262 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1264 status = _("Not connected")
1265 server = wallet.server
1268 status = _("Please choose a server.")
1269 server = random.choice( DEFAULT_SERVERS )
1271 if not wallet.interface.servers:
1273 for x in DEFAULT_SERVERS:
1274 h,port,protocol = x.split(':')
1275 servers_list.append( (h,[(protocol,port)] ) )
1277 servers_list = wallet.interface.servers
1280 for item in servers_list:
1284 protocol, port = item2
1290 d.setWindowTitle(_('Server'))
1291 d.setMinimumSize(375, 20)
1293 vbox = QVBoxLayout()
1296 hbox = QHBoxLayout()
1298 l.setPixmap(QPixmap(":icons/network.png"))
1300 hbox.addWidget(QLabel(status))
1302 vbox.addLayout(hbox)
1304 hbox = QHBoxLayout()
1305 host_line = QLineEdit()
1306 host_line.setText(server)
1307 hbox.addWidget(QLabel(_('Connect to') + ':'))
1308 hbox.addWidget(host_line)
1309 vbox.addLayout(hbox)
1311 hbox = QHBoxLayout()
1313 buttonGroup = QGroupBox(_("Protocol"))
1314 radio1 = QRadioButton("tcp", buttonGroup)
1315 radio2 = QRadioButton("http", buttonGroup)
1318 return unicode(host_line.text()).split(':')
1320 def set_button(protocol):
1322 radio1.setChecked(1)
1323 elif protocol == 'h':
1324 radio2.setChecked(1)
1326 def set_protocol(protocol):
1327 host = current_line()[0]
1329 if protocol not in pp.keys():
1330 protocol = pp.keys()[0]
1331 set_button(protocol)
1333 host_line.setText( host + ':' + port + ':' + protocol)
1335 radio1.clicked.connect(lambda x: set_protocol('t') )
1336 radio2.clicked.connect(lambda x: set_protocol('h') )
1338 set_button(current_line()[2])
1340 hbox.addWidget(QLabel(_('Protocol')+':'))
1341 hbox.addWidget(radio1)
1342 hbox.addWidget(radio2)
1344 vbox.addLayout(hbox)
1346 if wallet.interface.servers:
1347 label = _('Active Servers')
1349 label = _('Default Servers')
1351 servers_list_widget = QTreeWidget(parent)
1352 servers_list_widget.setHeaderLabels( [ label ] )
1353 servers_list_widget.setMaximumHeight(150)
1354 for host in plist.keys():
1355 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1358 host = unicode(x.text(0))
1360 if 't' in pp.keys():
1363 protocol = pp.keys()[0]
1365 host_line.setText( host + ':' + port + ':' + protocol)
1366 set_button(protocol)
1368 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1369 vbox.addWidget(servers_list_widget)
1371 vbox.addLayout(ok_cancel_buttons(d))
1374 if not d.exec_(): return
1375 server = unicode( host_line.text() )
1378 wallet.set_server(server)
1380 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1390 class ElectrumGui():
1392 def __init__(self, wallet):
1393 self.wallet = wallet
1394 self.app = QApplication(sys.argv)
1396 def waiting_dialog(self):
1402 w.setWindowTitle('Electrum')
1404 vbox = QVBoxLayout()
1409 if self.wallet.up_to_date:
1412 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1413 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1415 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1416 self.wallet.interface.poke()
1421 def restore_or_create(self):
1423 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1424 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1425 if r==2: return False
1427 is_recovery = (r==1)
1428 wallet = self.wallet
1429 # ask for the server.
1430 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1433 wallet.new_seed(None)
1434 wallet.init_mpk( wallet.seed )
1435 wallet.up_to_date_event.clear()
1436 wallet.up_to_date = False
1437 self.waiting_dialog()
1438 # run a dialog indicating the seed, ask the user to remember it
1439 ElectrumWindow.show_seed_dialog(wallet)
1441 ElectrumWindow.change_password_dialog(wallet)
1443 # ask for seed and gap.
1444 if not ElectrumWindow.seed_dialog( wallet ): return False
1445 wallet.init_mpk( wallet.seed )
1446 wallet.up_to_date_event.clear()
1447 wallet.up_to_date = False
1448 self.waiting_dialog()
1449 if wallet.is_found():
1450 # history and addressbook
1451 wallet.update_tx_history()
1452 wallet.fill_addressbook()
1453 print "recovery successful"
1456 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1464 w = ElectrumWindow(self.wallet)
1465 if url: w.set_url(url)