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()
57 pos = entry.cursorPosition()
59 if not is_int: chars +='.'
60 s = ''.join([i for i in text if i in chars])
65 s = s[:p] + '.' + s[p:p+8]
67 amount = int( Decimal(s) * 100000000 )
76 entry.setCursorPosition(pos)
80 class Timer(QtCore.QThread):
83 self.emit(QtCore.SIGNAL('timersignal'))
86 class HelpButton(QPushButton):
87 def __init__(self, text):
88 QPushButton.__init__(self, '?')
89 self.setFocusPolicy(Qt.NoFocus)
90 self.setFixedWidth(20)
91 self.clicked.connect(lambda: QMessageBox.information(self, 'Help', text, 'OK') )
94 class EnterButton(QPushButton):
95 def __init__(self, text, func):
96 QPushButton.__init__(self, text)
98 self.clicked.connect(func)
100 def keyPressEvent(self, e):
101 if e.key() == QtCore.Qt.Key_Return:
104 class MyTreeWidget(QTreeWidget):
105 def __init__(self, parent):
106 QTreeWidget.__init__(self, parent)
109 for i in range(0,self.viewport().height()/5):
110 if self.itemAt(QPoint(0,i*5)) == item:
114 for j in range(0,30):
115 if self.itemAt(QPoint(0,i*5 + j)) != item:
117 self.emit(SIGNAL('customContextMenuRequested(const QPoint&)'), QPoint(50, i*5 + j - 1))
119 self.connect(self, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), ddfr)
124 class StatusBarButton(QPushButton):
125 def __init__(self, icon, tooltip, func):
126 QPushButton.__init__(self, icon, '')
127 self.setToolTip(tooltip)
129 self.setMaximumWidth(25)
130 self.clicked.connect(func)
133 def keyPressEvent(self, e):
134 if e.key() == QtCore.Qt.Key_Return:
138 class QRCodeWidget(QWidget):
140 def __init__(self, addr):
141 super(QRCodeWidget, self).__init__()
142 self.setGeometry(300, 300, 350, 350)
145 def set_addr(self, addr):
147 self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L)
148 self.qr.addData(addr)
151 def paintEvent(self, e):
152 qp = QtGui.QPainter()
155 size = self.qr.getModuleCount()*boxsize
156 k = self.qr.getModuleCount()
157 black = QColor(0, 0, 0, 255)
158 white = QColor(255, 255, 255, 255)
161 if self.qr.isDark(r, c):
167 qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize)
172 def ok_cancel_buttons(dialog):
175 b = QPushButton("OK")
177 b.clicked.connect(dialog.accept)
178 b = QPushButton("Cancel")
180 b.clicked.connect(dialog.reject)
184 class ElectrumWindow(QMainWindow):
186 def __init__(self, wallet):
187 QMainWindow.__init__(self)
189 self.wallet.register_callback(self.update_callback)
191 self.funds_error = False
192 self.completions = QStringListModel()
194 self.tabs = tabs = QTabWidget(self)
195 tabs.addTab(self.create_history_tab(), _('History') )
197 tabs.addTab(self.create_send_tab(), _('Send') )
198 tabs.addTab(self.create_receive_tab(), _('Receive') )
199 tabs.addTab(self.create_contacts_tab(), _('Contacts') )
200 tabs.addTab(self.create_wall_tab(), _('Wall') )
201 tabs.setMinimumSize(600, 400)
202 tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
203 self.setCentralWidget(tabs)
204 self.create_status_bar()
205 self.setGeometry(100,100,840,400)
206 title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.wallet.path
207 if not self.wallet.seed: title += ' [seedless]'
208 self.setWindowTitle( title )
210 QShortcut(QKeySequence("Ctrl+W"), self, self.close)
211 QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
212 QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
213 QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
215 self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet)
216 self.history_list.setFocus(True)
218 # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913
219 if platform.system() == 'Windows':
220 n = 3 if self.wallet.seed else 2
221 tabs.setCurrentIndex (n)
222 tabs.setCurrentIndex (0)
225 def connect_slots(self, sender):
227 self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient)
228 self.previous_payto_e=''
230 def check_recipient(self):
231 if self.payto_e.hasFocus():
233 r = unicode( self.payto_e.text() )
234 if r != self.previous_payto_e:
235 self.previous_payto_e = r
237 if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
239 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
243 s = r + ' <' + to_address + '>'
244 self.payto_e.setText(s)
247 def update_callback(self):
248 self.emit(QtCore.SIGNAL('updatesignal'))
250 def update_wallet(self):
251 if self.wallet.interface and self.wallet.interface.is_connected:
252 if self.wallet.blocks == -1:
253 text = _( "Connecting..." )
254 icon = QIcon(":icons/status_disconnected.png")
255 elif self.wallet.blocks == 0:
256 text = _( "Server not ready" )
257 icon = QIcon(":icons/status_disconnected.png")
258 elif not self.wallet.up_to_date:
259 text = _( "Synchronizing..." )
260 icon = QIcon(":icons/status_waiting.png")
262 c, u = self.wallet.get_balance()
263 text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) )
264 if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() )
265 icon = QIcon(":icons/status_connected.png")
267 text = _( "Not connected" )
268 icon = QIcon(":icons/status_disconnected.png")
271 text = _( "Not enough funds" )
273 self.statusBar().showMessage(text)
274 self.status_button.setIcon( icon )
276 if self.wallet.up_to_date:
277 self.textbox.setText( self.wallet.banner )
278 self.update_history_tab()
279 self.update_receive_tab()
280 self.update_contacts_tab()
281 self.update_completions()
284 def create_history_tab(self):
285 self.history_list = l = MyTreeWidget(self)
287 l.setColumnWidth(0, 40)
288 l.setColumnWidth(1, 140)
289 l.setColumnWidth(2, 350)
290 l.setColumnWidth(3, 140)
291 l.setColumnWidth(4, 140)
292 l.setHeaderLabels( [ '', _( 'Date' ), _( 'To / From' ) , _('Amount'), _('Balance')] )
293 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
294 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
295 l.setContextMenuPolicy(Qt.CustomContextMenu)
296 l.customContextMenuRequested.connect(self.create_history_menu)
299 def create_history_menu(self, position):
300 self.history_list.selectedIndexes()
301 item = self.history_list.currentItem()
303 tx_hash = str(item.toolTip(0))
305 menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
306 menu.addAction(_("Details"), lambda: self.tx_details(tx_hash))
307 menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
308 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
310 def tx_details(self, tx_hash):
311 tx = self.wallet.tx_history.get(tx_hash)
314 conf = self.wallet.blocks - tx['height'] + 1
315 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
320 tx_details = _("Transaction Details") +"\n\n" \
321 + "Transaction ID:\n" + tx_hash + "\n\n" \
322 + "Status: %d confirmations\n\n"%conf \
323 + "Date: %s\n\n"%time_str \
324 + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
325 + "Outputs:\n-"+ '\n-'.join(tx['outputs'])
327 r = self.wallet.receipts.get(tx_hash)
329 tx_details += "\n_______________________________________" \
330 + '\n\nSigned URI: ' + r[2] \
331 + "\n\nSigned by: " + r[0] \
332 + '\n\nSignature: ' + r[1]
334 QMessageBox.information(self, 'Details', tx_details, 'OK')
337 def tx_label_clicked(self, item, column):
338 if column==2 and item.isSelected():
339 tx_hash = str(item.toolTip(0))
341 #if not self.wallet.labels.get(tx_hash): item.setText(2,'')
342 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
343 self.history_list.editItem( item, column )
344 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
347 def tx_label_changed(self, item, column):
351 tx_hash = str(item.toolTip(0))
352 tx = self.wallet.tx_history.get(tx_hash)
353 s = self.wallet.labels.get(tx_hash)
354 text = unicode( item.text(2) )
356 self.wallet.labels[tx_hash] = text
357 item.setForeground(2, QBrush(QColor('black')))
359 if s: self.wallet.labels.pop(tx_hash)
360 text = tx['default_label']
361 item.setText(2, text)
362 item.setForeground(2, QBrush(QColor('gray')))
365 def edit_label(self, is_recv):
366 l = self.receive_list if is_recv else self.contacts_list
367 c = 2 if is_recv else 1
368 item = l.currentItem()
369 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
370 l.editItem( item, c )
371 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
373 def address_label_clicked(self, item, column, l, column_addr, column_label):
374 if column==column_label and item.isSelected():
375 addr = unicode( item.text(column_addr) )
376 label = unicode( item.text(column_label) )
377 if label in self.wallet.aliases.keys():
379 item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
380 l.editItem( item, column )
381 item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
383 def address_label_changed(self, item, column, l, column_addr, column_label):
384 addr = unicode( item.text(column_addr) )
385 text = unicode( item.text(column_label) )
387 if text not in self.wallet.aliases.keys():
388 self.wallet.labels[addr] = text
390 print_error("Error: This is one of your aliases")
391 label = self.wallet.labels.get(addr,'')
392 item.setText(column_label, QString(label))
394 s = self.wallet.labels.get(addr)
395 if s: self.wallet.labels.pop(addr)
397 self.update_history_tab()
398 self.update_completions()
400 def update_history_tab(self):
401 self.history_list.clear()
403 for tx in self.wallet.get_tx_history():
404 tx_hash = tx['tx_hash']
406 conf = self.wallet.blocks - tx['height'] + 1
407 time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3]
408 icon = QIcon(":icons/confirmed.png")
412 icon = QIcon(":icons/unconfirmed.png")
415 label = self.wallet.labels.get(tx_hash)
416 is_default_label = (label == '') or (label is None)
417 if is_default_label: label = tx['default_label']
419 item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] )
420 item.setFont(2, QFont(MONOSPACE_FONT))
421 item.setFont(3, QFont(MONOSPACE_FONT))
422 item.setFont(4, QFont(MONOSPACE_FONT))
423 item.setToolTip(0, tx_hash)
425 item.setForeground(2, QBrush(QColor('grey')))
427 item.setIcon(0, icon)
428 self.history_list.insertTopLevelItem(0,item)
430 self.history_list.setCurrentItem(self.history_list.topLevelItem(0))
433 def create_send_tab(self):
438 grid.setColumnMinimumWidth(3,300)
439 grid.setColumnStretch(5,1)
441 self.payto_e = QLineEdit()
442 grid.addWidget(QLabel(_('Pay to')), 1, 0)
443 grid.addWidget(self.payto_e, 1, 1, 1, 3)
446 qrcode = qrscanner.scan_qr()
447 if 'address' in qrcode:
448 self.payto_e.setText(qrcode['address'])
449 if 'amount' in qrcode:
450 self.amount_e.setText(str(qrcode['amount']))
451 if 'label' in qrcode:
452 self.message_e.setText(qrcode['label'])
453 if 'message' in qrcode:
454 self.message_e.setText("%s (%s)" % (self.message_e.text(), qrcode['message']))
457 if qrscanner.is_available():
458 b = QPushButton(_("Scan QR code"))
459 b.clicked.connect(fill_from_qr)
460 grid.addWidget(b, 1, 5)
462 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)
464 completer = QCompleter()
465 completer.setCaseSensitivity(False)
466 self.payto_e.setCompleter(completer)
467 completer.setModel(self.completions)
469 self.message_e = QLineEdit()
470 grid.addWidget(QLabel(_('Description')), 2, 0)
471 grid.addWidget(self.message_e, 2, 1, 1, 3)
472 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)
474 self.amount_e = QLineEdit()
475 grid.addWidget(QLabel(_('Amount')), 3, 0)
476 grid.addWidget(self.amount_e, 3, 1, 1, 2)
477 grid.addWidget(HelpButton(
478 _('Amount to be sent.') + '\n\n' \
479 + _('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)
481 self.fee_e = QLineEdit()
482 grid.addWidget(QLabel(_('Fee')), 4, 0)
483 grid.addWidget(self.fee_e, 4, 1, 1, 2)
484 grid.addWidget(HelpButton(
485 _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
486 + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
487 + _('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)
489 b = EnterButton(_("Send"), self.do_send)
490 grid.addWidget(b, 6, 1)
492 b = EnterButton(_("Clear"),self.do_clear)
493 grid.addWidget(b, 6, 2)
495 self.payto_sig = QLabel('')
496 grid.addWidget(self.payto_sig, 7, 0, 1, 4)
498 QShortcut(QKeySequence("Up"), w, w.focusPreviousChild)
499 QShortcut(QKeySequence("Down"), w, w.focusNextChild)
508 def entry_changed( is_fee ):
509 self.funds_error = False
510 amount = numbify(self.amount_e)
511 fee = numbify(self.fee_e)
512 if not is_fee: fee = None
515 inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
517 self.fee_e.setText( str( Decimal( fee ) / 100000000 ) )
520 palette.setColor(self.amount_e.foregroundRole(), QColor('black'))
523 palette.setColor(self.amount_e.foregroundRole(), QColor('red'))
524 self.funds_error = True
525 self.amount_e.setPalette(palette)
526 self.fee_e.setPalette(palette)
528 self.amount_e.textChanged.connect(lambda: entry_changed(False) )
529 self.fee_e.textChanged.connect(lambda: entry_changed(True) )
534 def update_completions(self):
536 for addr,label in self.wallet.labels.items():
537 if addr in self.wallet.addressbook:
538 l.append( label + ' <' + addr + '>')
539 l = l + self.wallet.aliases.keys()
541 self.completions.setStringList(l)
547 label = unicode( self.message_e.text() )
548 r = unicode( self.payto_e.text() )
552 m1 = re.match(ALIAS_REGEXP, r)
553 # label or alias, with address in brackets
554 m2 = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
557 to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
561 to_address = m2.group(2)
565 if not self.wallet.is_valid(to_address):
566 QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
570 amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 )
572 QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
575 fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 )
577 QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
580 if self.wallet.use_encryption:
581 password = self.password_dialog()
588 tx = self.wallet.mktx( to_address, amount, label, password, fee)
589 except BaseException, e:
590 self.show_message(str(e))
593 status, msg = self.wallet.sendtx( tx )
595 QMessageBox.information(self, '', _('Payment sent.')+'\n'+msg, _('OK'))
597 self.update_contacts_tab()
599 QMessageBox.warning(self, _('Error'), msg, _('OK'))
602 def set_url(self, url):
603 payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
604 self.tabs.setCurrentIndex(1)
605 label = self.wallet.labels.get(payto)
606 m_addr = label + ' <'+ payto+'>' if label else payto
607 self.payto_e.setText(m_addr)
609 self.message_e.setText(message)
610 self.amount_e.setText(amount)
612 self.set_frozen(self.payto_e,True)
613 self.set_frozen(self.amount_e,True)
614 self.set_frozen(self.message_e,True)
615 self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity )
617 self.payto_sig.setVisible(False)
620 self.payto_sig.setVisible(False)
621 for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
623 self.set_frozen(e,False)
625 def set_frozen(self,entry,frozen):
627 entry.setReadOnly(True)
628 entry.setFrame(False)
630 palette.setColor(entry.backgroundRole(), QColor('lightgray'))
631 entry.setPalette(palette)
633 entry.setReadOnly(False)
636 palette.setColor(entry.backgroundRole(), QColor('white'))
637 entry.setPalette(palette)
640 def toggle_freeze(self,addr):
642 if addr in self.wallet.frozen_addresses:
643 self.wallet.unfreeze(addr)
645 self.wallet.freeze(addr)
646 self.update_receive_tab()
648 def toggle_priority(self,addr):
650 if addr in self.wallet.prioritized_addresses:
651 self.wallet.unprioritize(addr)
653 self.wallet.prioritize(addr)
654 self.update_receive_tab()
657 def create_list_tab(self, headers):
658 "generic tab creation method"
659 l = MyTreeWidget(self)
660 l.setColumnCount( len(headers) )
661 l.setHeaderLabels( headers )
671 vbox.addWidget(buttons)
676 buttons.setLayout(hbox)
681 def create_receive_tab(self):
682 l,w,hbox = self.create_list_tab([_('Flags'), _('Address'), _('Label'), _('Balance'), _('Tx')])
683 l.setContextMenuPolicy(Qt.CustomContextMenu)
684 l.customContextMenuRequested.connect(self.create_receive_menu)
685 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,1,2))
686 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,1,2))
687 self.receive_list = l
688 self.receive_buttons_hbox = hbox
692 def create_contacts_tab(self):
693 l,w,hbox = self.create_list_tab([_('Address'), _('Label'), _('Tx')])
694 l.setContextMenuPolicy(Qt.CustomContextMenu)
695 l.customContextMenuRequested.connect(self.create_contact_menu)
696 self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l,0,1))
697 self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l,0,1))
698 self.contacts_list = l
699 self.contacts_buttons_hbox = hbox
700 hbox.addWidget(EnterButton(_("New"), self.new_contact_dialog))
705 def create_receive_menu(self, position):
706 # fixme: this function apparently has a side effect.
707 # if it is not called the menu pops up several times
708 #self.receive_list.selectedIndexes()
710 item = self.receive_list.itemAt(position)
712 addr = unicode(item.text(1))
714 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
715 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
716 menu.addAction(_("Edit label"), lambda: self.edit_label(True))
717 if self.wallet.expert_mode:
718 t = _("Unfreeze") if addr in self.wallet.frozen_addresses else _("Freeze")
719 menu.addAction(t, lambda: self.toggle_freeze(addr))
720 t = _("Unprioritize") if addr in self.wallet.prioritized_addresses else _("Prioritize")
721 menu.addAction(t, lambda: self.toggle_priority(addr))
722 menu.exec_(self.receive_list.viewport().mapToGlobal(position))
725 def payto(self, x, is_alias):
732 label = self.wallet.labels.get(addr)
733 m_addr = label + ' <' + addr + '>' if label else addr
734 self.tabs.setCurrentIndex(1)
735 self.payto_e.setText(m_addr)
736 self.amount_e.setFocus()
738 def delete_contact(self, x, is_alias):
739 if self.question("Do you want to remove %s from your list of contacts?"%x):
740 if not is_alias and x in self.wallet.addressbook:
741 self.wallet.addressbook.remove(x)
742 if x in self.wallet.labels.keys():
743 self.wallet.labels.pop(x)
744 elif is_alias and x in self.wallet.aliases:
745 self.wallet.aliases.pop(x)
746 self.update_history_tab()
747 self.update_contacts_tab()
748 self.update_completions()
750 def create_contact_menu(self, position):
751 # fixme: this function apparently has a side effect.
752 # if it is not called the menu pops up several times
753 #self.contacts_list.selectedIndexes()
755 item = self.contacts_list.itemAt(position)
757 addr = unicode(item.text(0))
758 label = unicode(item.text(1))
759 is_alias = label in self.wallet.aliases.keys()
760 x = label if is_alias else addr
762 menu.addAction(_("Copy to Clipboard"), lambda: self.app.clipboard().setText(addr))
763 menu.addAction(_("Pay to"), lambda: self.payto(x, is_alias))
764 menu.addAction(_("View QR code"),lambda: self.show_address_qrcode(addr))
766 menu.addAction(_("Edit label"), lambda: self.edit_label(False))
768 menu.addAction(_("View alias details"), lambda: self.show_contact_details(label))
769 menu.addAction(_("Delete"), lambda: self.delete_contact(x,is_alias))
770 menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
773 def update_receive_tab(self):
774 l = self.receive_list
776 l.setColumnHidden(0,not self.wallet.expert_mode)
777 l.setColumnHidden(3,not self.wallet.expert_mode)
778 l.setColumnHidden(4,not self.wallet.expert_mode)
779 l.setColumnWidth(0, 50)
780 l.setColumnWidth(1, 310)
781 l.setColumnWidth(2, 250)
782 l.setColumnWidth(3, 130)
783 l.setColumnWidth(4, 10)
787 for address in self.wallet.all_addresses():
789 if self.wallet.is_change(address) and not self.wallet.expert_mode:
792 label = self.wallet.labels.get(address,'')
794 h = self.wallet.history.get(address,[])
796 if not item['is_input'] : n=n+1
800 if address in self.wallet.addresses:
802 if gap > self.wallet.gap_limit:
805 if address in self.wallet.addresses:
808 c, u = self.wallet.get_addr_balance(address)
809 balance = format_satoshis( c + u, False, self.wallet.num_zeros )
810 flags = self.wallet.get_address_flags(address)
811 item = QTreeWidgetItem( [ flags, address, label, balance, tx] )
813 item.setFont(0, QFont(MONOSPACE_FONT))
814 item.setFont(1, QFont(MONOSPACE_FONT))
815 item.setFont(3, QFont(MONOSPACE_FONT))
816 if address in self.wallet.frozen_addresses:
817 item.setBackgroundColor(1, QColor('lightblue'))
818 elif address in self.wallet.prioritized_addresses:
819 item.setBackgroundColor(1, QColor('lightgreen'))
820 if is_red and address in self.wallet.addresses:
821 item.setBackgroundColor(1, QColor('red'))
822 l.addTopLevelItem(item)
824 # we use column 1 because column 0 may be hidden
825 l.setCurrentItem(l.topLevelItem(0),1)
827 def show_contact_details(self, m):
828 a = self.wallet.aliases.get(m)
830 if a[0] in self.wallet.authorities.keys():
831 s = self.wallet.authorities.get(a[0])
834 msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
835 QMessageBox.information(self, 'Alias', msg, 'OK')
837 def update_contacts_tab(self):
839 l = self.contacts_list
841 l.setColumnHidden(2, not self.wallet.expert_mode)
842 l.setColumnWidth(0, 350)
843 l.setColumnWidth(1, 330)
844 l.setColumnWidth(2, 100)
847 for alias, v in self.wallet.aliases.items():
849 alias_targets.append(target)
850 item = QTreeWidgetItem( [ target, alias, '-'] )
851 item.setBackgroundColor(0, QColor('lightgray'))
852 l.addTopLevelItem(item)
854 for address in self.wallet.addressbook:
855 if address in alias_targets: continue
856 label = self.wallet.labels.get(address,'')
858 for item in self.wallet.tx_history.values():
859 if address in item['outputs'] : n=n+1
861 item = QTreeWidgetItem( [ address, label, tx] )
862 item.setFont(0, QFont(MONOSPACE_FONT))
863 l.addTopLevelItem(item)
865 l.setCurrentItem(l.topLevelItem(0))
867 def create_wall_tab(self):
868 self.textbox = textbox = QTextEdit(self)
869 textbox.setFont(QFont(MONOSPACE_FONT))
870 textbox.setReadOnly(True)
873 def create_status_bar(self):
875 sb.setFixedHeight(35)
877 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) )
878 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) )
880 sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) )
881 self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) )
882 sb.addPermanentWidget( self.status_button )
883 self.setStatusBar(sb)
885 def new_contact_dialog(self):
886 text, ok = QInputDialog.getText(self, _('New Contact'), _('Address') + ':')
887 address = unicode(text)
889 if self.wallet.is_valid(address):
890 self.wallet.addressbook.append(address)
892 self.update_contacts_tab()
893 self.update_history_tab()
894 self.update_completions()
896 QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
899 def show_seed_dialog(wallet, parent=None):
901 QMessageBox.information(parent, _('Message'),
902 _('No seed'), _('OK'))
905 if wallet.use_encryption:
906 password = parent.password_dialog()
913 seed = wallet.pw_decode(wallet.seed, password)
915 QMessageBox.warning(parent, _('Error'),
916 _('Incorrect Password'), _('OK'))
919 dialog = QDialog(None)
921 dialog.setWindowTitle(_("Seed"))
923 brainwallet = ' '.join(mnemonic.mn_encode(seed))
925 msg = _('<p>"%s"</p>'
926 "<p>If you memorise or write down these 12 words, you will always be able to recover your wallet.</p>"
927 "<p>This is called a 'BrainWallet'. The order of words is important. Case does not matter (capitals or lowercase).</p>") % brainwallet
928 main_text = QLabel(msg)
929 main_text.setWordWrap(True)
932 logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
939 copy_function = lambda: app.clipboard().setText(brainwallet)
940 copy_button = QPushButton(_("Copy to Clipboard"))
941 copy_button.clicked.connect(copy_function)
943 show_qr_function = lambda: ElectrumWindow.show_seed_qrcode(seed)
944 qr_button = QPushButton(_("View as QR Code"))
945 qr_button.clicked.connect(show_qr_function)
947 ok_button = QPushButton(_("OK"))
948 ok_button.clicked.connect(dialog.accept)
950 main_layout = QGridLayout()
951 main_layout.addWidget(logo, 0, 0)
952 main_layout.addWidget(main_text, 0, 1, 1, -1)
953 main_layout.addWidget(copy_button, 1, 1)
954 main_layout.addWidget(qr_button, 1, 2)
955 main_layout.addWidget(ok_button, 1, 3)
956 dialog.setLayout(main_layout)
961 def show_seed_qrcode(seed):
965 d.setWindowTitle(_("Seed"))
966 d.setMinimumSize(270, 300)
968 vbox.addWidget(QRCodeWidget(seed))
971 b = QPushButton(_("OK"))
973 b.clicked.connect(d.accept)
980 def show_address_qrcode(self,address):
981 if not address: return
984 d.setWindowTitle(address)
985 d.setMinimumSize(270, 350)
987 qrw = QRCodeWidget(address)
991 amount_e = QLineEdit()
992 hbox.addWidget(QLabel(_('Amount')))
993 hbox.addWidget(amount_e)
996 #hbox = QHBoxLayout()
997 #label_e = QLineEdit()
998 #hbox.addWidget(QLabel('Label'))
999 #hbox.addWidget(label_e)
1000 #vbox.addLayout(hbox)
1002 def amount_changed():
1003 amount = numbify(amount_e)
1004 #label = str( label_e.getText() )
1005 if amount is not None:
1006 qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000)))
1008 qrw.set_addr( address )
1012 bmp.save_qrcode(qrw.qr, "qrcode.bmp")
1013 self.show_message(_("QR code saved to file") + " 'qrcode.bmp'")
1015 amount_e.textChanged.connect( amount_changed )
1017 hbox = QHBoxLayout()
1019 b = QPushButton(_("Save"))
1020 b.clicked.connect(do_save)
1022 b = QPushButton(_("Close"))
1024 b.clicked.connect(d.accept)
1026 vbox.addLayout(hbox)
1030 def question(self, msg):
1031 return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
1033 def show_message(self, msg):
1034 QMessageBox.information(self, _('Message'), msg, _('OK'))
1036 def password_dialog(self ):
1043 vbox = QVBoxLayout()
1044 msg = _('Please enter your password')
1045 vbox.addWidget(QLabel(msg))
1047 grid = QGridLayout()
1049 grid.addWidget(QLabel(_('Password')), 1, 0)
1050 grid.addWidget(pw, 1, 1)
1051 vbox.addLayout(grid)
1053 vbox.addLayout(ok_cancel_buttons(d))
1056 if not d.exec_(): return
1057 return unicode(pw.text())
1064 def change_password_dialog( wallet, parent=None ):
1067 QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
1075 new_pw = QLineEdit()
1076 new_pw.setEchoMode(2)
1077 conf_pw = QLineEdit()
1078 conf_pw.setEchoMode(2)
1080 vbox = QVBoxLayout()
1082 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')
1084 msg = _("Please choose a password to encrypt your wallet keys.")+'\n'+_("Leave these fields empty if you want to disable encryption.")
1085 vbox.addWidget(QLabel(msg))
1087 grid = QGridLayout()
1090 if wallet.use_encryption:
1091 grid.addWidget(QLabel(_('Password')), 1, 0)
1092 grid.addWidget(pw, 1, 1)
1094 grid.addWidget(QLabel(_('New Password')), 2, 0)
1095 grid.addWidget(new_pw, 2, 1)
1097 grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
1098 grid.addWidget(conf_pw, 3, 1)
1099 vbox.addLayout(grid)
1101 vbox.addLayout(ok_cancel_buttons(d))
1104 if not d.exec_(): return
1106 password = unicode(pw.text()) if wallet.use_encryption else None
1107 new_password = unicode(new_pw.text())
1108 new_password2 = unicode(conf_pw.text())
1111 seed = wallet.pw_decode( wallet.seed, password)
1113 QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
1116 if new_password != new_password2:
1117 QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
1120 wallet.update_password(seed, password, new_password)
1123 def seed_dialog(wallet, parent=None):
1127 vbox = QVBoxLayout()
1128 msg = _("Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet.")
1129 vbox.addWidget(QLabel(msg))
1131 grid = QGridLayout()
1134 seed_e = QLineEdit()
1135 grid.addWidget(QLabel(_('Seed or mnemonic')), 1, 0)
1136 grid.addWidget(seed_e, 1, 1)
1140 grid.addWidget(QLabel(_('Gap limit')), 2, 0)
1141 grid.addWidget(gap_e, 2, 1)
1142 gap_e.textChanged.connect(lambda: numbify(gap_e,True))
1143 vbox.addLayout(grid)
1145 vbox.addLayout(ok_cancel_buttons(d))
1148 if not d.exec_(): return
1151 gap = int(unicode(gap_e.text()))
1153 QMessageBox.warning(None, _('Error'), 'error', 'OK')
1157 seed = unicode(seed_e.text())
1160 print_error("Warning: Not hex, trying decode")
1162 seed = mnemonic.mn_decode( seed.split(' ') )
1164 QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
1167 QMessageBox.warning(None, _('Error'), _('No seed'), 'OK')
1170 wallet.seed = str(seed)
1171 #print repr(wallet.seed)
1172 wallet.gap_limit = gap
1176 def set_expert_mode(self, b):
1177 self.wallet.expert_mode = b
1179 self.update_receive_tab()
1180 self.update_contacts_tab()
1181 # if self.wallet.seed:
1182 # self.nochange_cb.setHidden(not self.wallet.expert_mode)
1185 def settings_dialog(self):
1188 vbox = QVBoxLayout()
1189 msg = _('Here are the settings of your wallet.') + '\n'\
1190 + _('For more explanations, click on the help buttons next to each field.')
1193 label.setFixedWidth(250)
1194 label.setWordWrap(True)
1195 label.setAlignment(Qt.AlignJustify)
1196 vbox.addWidget(label)
1198 grid = QGridLayout()
1200 vbox.addLayout(grid)
1203 fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
1204 grid.addWidget(QLabel(_('Transaction fee')), 2, 0)
1205 grid.addWidget(fee_e, 2, 1)
1206 msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \
1207 + _('Recommended value') + ': 0.001'
1208 grid.addWidget(HelpButton(msg), 2, 2)
1209 fee_e.textChanged.connect(lambda: numbify(fee_e,False))
1212 nz_e.setText("%d"% self.wallet.num_zeros)
1213 grid.addWidget(QLabel(_('Display zeros')), 3, 0)
1214 msg = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
1215 grid.addWidget(HelpButton(msg), 3, 2)
1216 grid.addWidget(nz_e, 3, 1)
1217 nz_e.textChanged.connect(lambda: numbify(nz_e,True))
1219 cb = QCheckBox(_('Expert mode'))
1220 grid.addWidget(cb, 4, 0)
1221 cb.setChecked(self.wallet.expert_mode)
1223 if self.wallet.expert_mode:
1225 usechange_cb = QCheckBox(_('Use change addresses'))
1226 grid.addWidget(usechange_cb, 5, 0)
1227 usechange_cb.setChecked(self.wallet.use_change)
1228 grid.addWidget(HelpButton(_('Using a change addresses makes it more difficult for other people to track your transactions. ')), 5, 2)
1230 msg = _('The gap limit is the maximal number of contiguous unused addresses in your sequence of receiving addresses.') + '\n' \
1231 + _('You may increase it if you need more receiving addresses.') + '\n\n' \
1232 + _('Your current gap limit is') + ': %d'%self.wallet.gap_limit + '\n' \
1233 + _('Given the current status of your address sequence, the minimum gap limit you can use is: ') + '%d'%self.wallet.min_acceptable_gap() + '\n\n' \
1234 + _('Warning') + ': ' \
1235 + _('The gap limit parameter must be provided in order to recover your wallet from seed.') + ' ' \
1236 + _('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'
1238 gap_e.setText("%d"% self.wallet.gap_limit)
1239 grid.addWidget(QLabel(_('Gap limit')), 6, 0)
1240 grid.addWidget(gap_e, 6, 1)
1241 grid.addWidget(HelpButton(msg), 6, 2)
1242 gap_e.textChanged.connect(lambda: numbify(nz_e,True))
1245 vbox.addLayout(ok_cancel_buttons(d))
1249 if not d.exec_(): return
1251 fee = unicode(fee_e.text())
1253 fee = int( 100000000 * Decimal(fee) )
1255 QMessageBox.warning(self, _('Error'), _('Invalid value') +': %s'%fee, _('OK'))
1258 if self.wallet.fee != fee:
1259 self.wallet.fee = fee
1262 nz = unicode(nz_e.text())
1267 QMessageBox.warning(self, _('Error'), _('Invalid value')+':%s'%nz, _('OK'))
1270 if self.wallet.num_zeros != nz:
1271 self.wallet.num_zeros = nz
1272 self.update_history_tab()
1273 self.update_receive_tab()
1276 if self.wallet.expert_mode:
1278 self.wallet.use_change = usechange_cb.isChecked()
1281 n = int(gap_e.text())
1283 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1285 if self.wallet.gap_limit != n:
1286 r = self.wallet.change_gap_limit(n)
1288 self.update_receive_tab()
1290 QMessageBox.warning(self, _('Error'), _('Invalid value'), _('OK'))
1292 self.set_expert_mode(cb.isChecked())
1296 def network_dialog(wallet, parent=None):
1297 interface = wallet.interface
1299 if interface.is_connected:
1300 status = _("Connected to")+" %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks)
1302 status = _("Not connected")
1303 server = wallet.server
1306 status = _("Please choose a server.")
1307 server = random.choice( DEFAULT_SERVERS )
1309 if not wallet.interface.servers:
1311 for x in DEFAULT_SERVERS:
1312 h,port,protocol = x.split(':')
1313 servers_list.append( (h,[(protocol,port)] ) )
1315 servers_list = wallet.interface.servers
1318 for item in servers_list:
1322 protocol, port = item2
1328 d.setWindowTitle(_('Server'))
1329 d.setMinimumSize(375, 20)
1331 vbox = QVBoxLayout()
1334 hbox = QHBoxLayout()
1336 l.setPixmap(QPixmap(":icons/network.png"))
1338 hbox.addWidget(QLabel(status))
1340 vbox.addLayout(hbox)
1342 hbox = QHBoxLayout()
1343 host_line = QLineEdit()
1344 host_line.setText(server)
1345 hbox.addWidget(QLabel(_('Connect to') + ':'))
1346 hbox.addWidget(host_line)
1347 vbox.addLayout(hbox)
1349 hbox = QHBoxLayout()
1351 buttonGroup = QGroupBox(_("Protocol"))
1352 radio1 = QRadioButton("tcp", buttonGroup)
1353 radio2 = QRadioButton("http", buttonGroup)
1356 return unicode(host_line.text()).split(':')
1358 def set_button(protocol):
1360 radio1.setChecked(1)
1361 elif protocol == 'h':
1362 radio2.setChecked(1)
1364 def set_protocol(protocol):
1365 host = current_line()[0]
1367 if protocol not in pp.keys():
1368 protocol = pp.keys()[0]
1369 set_button(protocol)
1371 host_line.setText( host + ':' + port + ':' + protocol)
1373 radio1.clicked.connect(lambda x: set_protocol('t') )
1374 radio2.clicked.connect(lambda x: set_protocol('h') )
1376 set_button(current_line()[2])
1378 hbox.addWidget(QLabel(_('Protocol')+':'))
1379 hbox.addWidget(radio1)
1380 hbox.addWidget(radio2)
1382 vbox.addLayout(hbox)
1384 hbox = QHBoxLayout()
1385 proxy_mode = QComboBox()
1386 proxy_host = QLineEdit()
1387 proxy_port = QLineEdit()
1388 proxy_mode.addItems(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
1389 proxy_mode.setCurrentIndex(proxy_mode.findText(str(interface.proxy["mode"]).upper()))
1390 proxy_host.setText(interface.proxy["host"])
1391 proxy_port.setText(interface.proxy["port"])
1392 hbox.addWidget(QLabel(_('Proxy') + ':'))
1393 hbox.addWidget(proxy_mode)
1394 hbox.addWidget(proxy_host)
1395 hbox.addWidget(proxy_port)
1396 vbox.addLayout(hbox)
1398 hbox = QHBoxLayout()
1400 if wallet.interface.servers:
1401 label = _('Active Servers')
1403 label = _('Default Servers')
1405 servers_list_widget = QTreeWidget(parent)
1406 servers_list_widget.setHeaderLabels( [ label ] )
1407 servers_list_widget.setMaximumHeight(150)
1408 for host in plist.keys():
1409 servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] ))
1412 host = unicode(x.text(0))
1414 if 't' in pp.keys():
1417 protocol = pp.keys()[0]
1419 host_line.setText( host + ':' + port + ':' + protocol)
1420 set_button(protocol)
1422 servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line)
1423 vbox.addWidget(servers_list_widget)
1425 vbox.addLayout(ok_cancel_buttons(d))
1428 if not d.exec_(): return
1429 server = unicode( host_line.text() )
1432 proxy = { u'mode':unicode(proxy_mode.currentText()).lower(), u'host':unicode(proxy_host.text()), u'port':unicode(proxy_port.text()) }
1433 wallet.set_server(server, proxy)
1435 QMessageBox.information(None, _('Error'), 'error', _('OK'))
1447 def __init__(self, wallet, app=None):
1448 self.wallet = wallet
1450 self.app = QApplication(sys.argv)
1452 def server_list_changed(self):
1455 def waiting_dialog(self):
1461 w.setWindowTitle('Electrum')
1463 vbox = QVBoxLayout()
1468 if self.wallet.up_to_date:
1471 l.setText("Please wait...\nAddresses generated: %d\nKilobytes received: %.1f"\
1472 %(len(self.wallet.all_addresses()), self.wallet.interface.bytes_received/1024.))
1474 w.connect(s, QtCore.SIGNAL('timersignal'), f)
1475 self.wallet.interface.poke()
1480 def restore_or_create(self):
1482 msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
1483 r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
1484 if r==2: return False
1486 is_recovery = (r==1)
1487 wallet = self.wallet
1488 # ask for the server.
1489 if not ElectrumWindow.network_dialog( wallet, parent=None ): return False
1492 wallet.new_seed(None)
1493 wallet.init_mpk( wallet.seed )
1494 wallet.up_to_date_event.clear()
1495 wallet.up_to_date = False
1496 self.waiting_dialog()
1497 # run a dialog indicating the seed, ask the user to remember it
1498 ElectrumWindow.show_seed_dialog(wallet)
1500 ElectrumWindow.change_password_dialog(wallet)
1502 # ask for seed and gap.
1503 if not ElectrumWindow.seed_dialog( wallet ): return False
1504 wallet.init_mpk( wallet.seed )
1505 wallet.up_to_date_event.clear()
1506 wallet.up_to_date = False
1507 self.waiting_dialog()
1508 if wallet.is_found():
1509 # history and addressbook
1510 wallet.update_tx_history()
1511 wallet.fill_addressbook()
1512 print "Recovery successful"
1515 QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
1523 w = ElectrumWindow(self.wallet)
1524 if url: w.set_url(url)